├── .gitignore ├── LICENSE.txt ├── Rakefile ├── changelog.md ├── pom.xml ├── readme.md └── src ├── main ├── java │ ├── com │ │ └── octopusdeploy │ │ │ └── api │ │ │ ├── AuthenticatedWebClient.java │ │ │ ├── ChannelsApi.java │ │ │ ├── DeploymentsApi.java │ │ │ ├── EnvironmentsApi.java │ │ │ ├── ErrorParser.java │ │ │ ├── OctopusApi.java │ │ │ ├── ProjectsApi.java │ │ │ ├── ReleasesApi.java │ │ │ ├── TasksApi.java │ │ │ ├── TenantsApi.java │ │ │ ├── VariablesApi.java │ │ │ └── data │ │ │ ├── Channel.java │ │ │ ├── DeploymentProcess.java │ │ │ ├── DeploymentProcessStep.java │ │ │ ├── DeploymentProcessStepAction.java │ │ │ ├── DeploymentProcessTemplate.java │ │ │ ├── Environment.java │ │ │ ├── Project.java │ │ │ ├── Release.java │ │ │ ├── SelectedPackage.java │ │ │ ├── Task.java │ │ │ ├── Tenant.java │ │ │ └── Variable.java │ └── hudson │ │ └── plugins │ │ └── octopusdeploy │ │ ├── AbstractOctopusDeployRecorder.java │ │ ├── BuildInfoSummary.java │ │ ├── EnvironmentVariableValueInjector.java │ │ ├── JSONSanitizer.java │ │ ├── Log.java │ │ ├── OctopusDeployDeploymentRecorder.java │ │ ├── OctopusDeployPlugin.java │ │ ├── OctopusDeployReleaseRecorder.java │ │ ├── OctopusDeployServer.java │ │ ├── OctopusValidator.java │ │ └── PackageConfiguration.java ├── resources │ ├── hudson │ │ └── plugins │ │ │ └── octopusdeploy │ │ │ ├── BuildInfoSummary │ │ │ ├── badge.jelly │ │ │ └── summary.jelly │ │ │ ├── OctopusDeployDeploymentRecorder │ │ │ ├── config.jelly │ │ │ ├── global.jelly │ │ │ ├── help-environment.html │ │ │ ├── help-project.html │ │ │ ├── help-releaseVersion.html │ │ │ ├── help-serverId.html │ │ │ ├── help-tenant.html │ │ │ ├── help-variables.html │ │ │ └── help-waitForDeployment.html │ │ │ ├── OctopusDeployPlugin │ │ │ ├── config.jelly │ │ │ ├── global.jelly │ │ │ ├── help-apiKey.html │ │ │ ├── help-serverId.html │ │ │ └── help-url.html │ │ │ ├── OctopusDeployReleaseRecorder │ │ │ ├── config.jelly │ │ │ ├── global.jelly │ │ │ ├── help-channel.html │ │ │ ├── help-defaultPackageVersion.html │ │ │ ├── help-deployThisRelease.html │ │ │ ├── help-environment.html │ │ │ ├── help-jenkinsUrlLinkback.html │ │ │ ├── help-project.html │ │ │ ├── help-releaseNotes.html │ │ │ ├── help-releaseNotesFile.html │ │ │ ├── help-releaseNotesFromSCM.html │ │ │ ├── help-releaseVersion.html │ │ │ ├── help-serverId.html │ │ │ ├── help-tenant.html │ │ │ └── help-waitForDeployment.html │ │ │ └── PackageConfiguration │ │ │ ├── config.jelly │ │ │ ├── global.jelly │ │ │ ├── help-packageName.html │ │ │ ├── help-packageReferenceName.html │ │ │ └── help-packageVersion.html │ └── index.jelly └── webapp │ └── images │ ├── octopus-d.png │ ├── octopus-o.png │ └── octopus-r.png └── test └── java ├── com └── octopusdeploy │ └── api │ └── ErrorParserTest.java └── hudson └── plugins └── octopusdeploy └── JSONSanitizerTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | /target/* 2 | /work/* -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Cimpress 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/clean' 2 | 3 | CLEAN.include 'work', 'target' 4 | 5 | task :default => :bundle 6 | 7 | 8 | 9 | task :build do 10 | raise 'Build failed' unless system 'mvn compile' 11 | end 12 | 13 | task :bundle do 14 | raise 'Packaging failed!' unless system 'mvn package' 15 | end 16 | 17 | task :demo do 18 | raise 'Starting demo failed!' unless system 'mvn hpi:run' 19 | end -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Change Log # 2 | ## 1.9.0 ## 3 | * Passes "reference name" for the package on a step, now that there can be multiple packages per step (octopus 2018.8). 4 | 5 | ## 1.8.1 ## 6 | * Bugfix - When an Octopus url ended with / the url was truncated an extra character incorrectly 7 | 8 | ## 1.8.0 ## 9 | * Feature enhancement: Packages can now be specified by Package Name or Step Name 10 | 11 | ## 1.7.0 ## 12 | * Introduced multiple Octopus servers 13 | 14 | ## 1.6.0 ## 15 | * Implemented support for Channels 16 | * Refactored API 17 | 18 | ## 1.5.0 ## 19 | * Implemented support for multi-Tenant Deployment 20 | * Updated javadocs 21 | 22 | ## 1.4.0 ## 23 | * Set octopus variables for deployment 24 | 25 | ## 1.3.1 ## 26 | * Bugfix - Release notes from SCM now properly include changeset from current build 27 | 28 | ## 1.3.0 ## 29 | * Better error parsing from Octopus error page responses 30 | 31 | ## 1.2.1 ## 32 | * Internal release 33 | * Release and Deployment post-build steps 34 | * Build summary links 35 | * Build badge links 36 | * Global configuration items 37 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 24 | 25 | 4.0.0 26 | 27 | org.jenkins-ci.plugins 28 | plugin 29 | 2.5 30 | 31 | 32 | OctopusDeploy Plugin 33 | hudson.plugins.octopusdeploy 34 | octopusdeploy 35 | 1.10.0-SNAPSHOT 36 | hpi 37 | https://wiki.jenkins-ci.org/display/JENKINS/OctopusDeploy+Plugin 38 | 39 | 40 | scm:git:https://github.com/jenkinsci/octopusdeploy-plugin.git 41 | scm:git:https://git@github.com/jenkinsci/octopusdeploy-plugin.git 42 | https://github.com/jenkinsci/octopusdeploy-plugin 43 | octopusdeploy-1.5.0 44 | 45 | 46 | 47 | 48 | badriance 49 | Brian Adriance 50 | badriance@vistaprint.com 51 | 52 | 53 | jonlabroad 54 | Jon LaBroad 55 | jlabroad@vistaprint.com 56 | 57 | 58 | lteixeira 59 | Luis Teixeira 60 | lteixeira@vistaprint.com 61 | 62 | 63 | 64 | 65 | 1.625.3 66 | 7 67 | 2.13 68 | 1.121 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | repo.jenkins-ci.org 77 | https://repo.jenkins-ci.org/public/ 78 | 79 | 80 | 81 | 82 | 83 | repo.jenkins-ci.org 84 | https://repo.jenkins-ci.org/public/ 85 | 86 | 87 | 88 | 89 | 90 | 91 | org.codehaus.mojo 92 | findbugs-maven-plugin 93 | 3.0.4 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # OctopusDeploy Jenkins Plugin # 2 | Connecting [OctopusDeploy](https://octopusdeploy.com/) to the [Jenkins](https://jenkins-ci.org/) workflow. Integration with OctopusDeploy is achieved via the REST API, 3 | not the Octo.exe tool. 4 | 5 | ## Compatibility ## 6 | Tested and compatible with OctopusDeploy: 2.6 and 3.0 - 3.7. 7 | 8 | Tested and compatible with Octopus 2018.9 as of 1.9.0 of this plugin. 9 | 10 | # Main Components # 11 | ## Post-build step - Create Release ## 12 | Creates a new release entry for a given project. 13 | Optionally also deploys the newly created release to a given environment. 14 | 15 | This component will only run if the build has been successful up until the point the post-build step is invoked. 16 | 17 | ## Post-build step - Execute Deployment ## 18 | Sends a release to a given environment. 19 | 20 | This component will only run if the build has been successful up until the point the post-build step is invoked. 21 | 22 | # Features # 23 | ## REST API Integration ## 24 | This plugin implements part of the REST API for OctopusDeploy in order to retrieve data and execute commands on the server. 25 | It makes use of an API Key which is configured in the Global Settings Jenkins configuration page. This API Key is used for all interactions with OctopusDeploy. 26 | 27 | ## Environment Variable Support ## 28 | Environment variables can be used in many fields in the Release and Deployment components of this plugin. Any variable, written as ${VARIABLE_NAME} will get replaced 29 | by that variable's value. If there is no value set for that variable, the token is not replaced. 30 | 31 | ## Custom Release Notes ## 32 | The Release component allows Release Notes to be gathered from SCM, collecting commit messages from the current build all the way back to the last successful build, 33 | or it can load in Release Notes from a file. 34 | 35 | There is also an option to provide a link back to the job run that created the release in the Release Notes, allowing easy navigation between Jenkins jobs and 36 | OctopusDeploy Releases. 37 | 38 | ## Build Summary Links ## 39 | Each component provides a link to the Release or Deployment that it created. These links are provided in the console output of the job as well as showing up as a build 40 | badge, and in the Build Summary. 41 | 42 | ## Wait-for-Deployment ## 43 | The Deployment component can optionally wait for the completion of the Deployment, and record the status. If the status returns as failed, the Jenkins job will be marked 44 | as a failure. 45 | 46 | ## Autocomplete ## 47 | Some entry fields, like Project and Environment support auto-completion, pulling a list of names to choose from from the OctopusDeploy server. 48 | 49 | ## Octopus variables ## 50 | As of 1.4.0, this plugin can set Octopus variables for use in deployment. 51 | 52 | ## Multi tenant support ## 53 | As of 1.5.0, this plugin can submit a Tenant to the deployment step for use in Octopus' multi-tenant mode. 54 | 55 | ## Creating release on specific Channels ## 56 | As of 1.6.0, this plugin can create releases on specific Channels as defined by users. 57 | 58 | ## Multiple Octopus servers ## 59 | As of 1.7.0, this plugin now allows more than one Octopus server to be configured in the global Jenkins configuration. The selection of which Octopus server to use will be 60 | done by the plugin on a per-project basis (under Advanced Options). Note that unless otherwise specified, each project will use the first Octopus server listed. -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/AuthenticatedWebClient.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api; 2 | 3 | import java.io.*; 4 | import java.net.*; 5 | import java.nio.charset.Charset; 6 | import java.util.*; 7 | import org.apache.commons.lang.StringUtils; 8 | 9 | /** 10 | * An Octopus Deploy web API client that automatically puts the API key in a header 11 | * Offers GET and POST, returning the response as JSON. 12 | */ 13 | public class AuthenticatedWebClient { 14 | private static final String UTF8 = "UTF-8"; 15 | private static final String GET = "GET"; 16 | private static final String POST = "POST"; 17 | private static final String OCTOPUS_API_KEY_HEADER = "X-Octopus-ApiKey"; 18 | 19 | private final String hostUrl; 20 | private final String apiKey; 21 | 22 | /** 23 | * Create a new instance. 24 | * @param hostUrl URL to the Octopus Deploy host. example: https://octopus.company.com/ 25 | * @param apiKey The Octopus Deploy API key to use in making API requests 26 | */ 27 | public AuthenticatedWebClient(String hostUrl, String apiKey) { 28 | this.hostUrl = hostUrl; 29 | this.apiKey = apiKey; 30 | } 31 | 32 | /** 33 | * Executes a post against the resource provided. 34 | * Uses content type application/x-www-form-urlencoded 35 | * @param resource the URL to the resource (omitting the host portion) 36 | * @param data an encoded data array of the data to post 37 | * @return JSON blob representing the response from the server. 38 | * @throws ProtocolException if the operation is performed on a URL that is not HTTP or HTTPS 39 | * @throws IOException if there are errors establishing a web connection OR reading the output stream 40 | * @throws IllegalArgumentException When data to post is null 41 | */ 42 | public WebResponse post(String resource, byte[] data) throws ProtocolException, IOException 43 | { 44 | if (data == null) 45 | { 46 | throw new IllegalArgumentException("Data to post can not be null"); 47 | } 48 | URLConnection connection = getConnection(POST, resource, null); 49 | connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); 50 | connection.setRequestProperty("Content-Length", Integer.toString(data.length)); 51 | connection.setDoOutput(true); 52 | connection.connect(); 53 | DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream()); 54 | dataOutputStream.write(data); 55 | dataOutputStream.flush(); 56 | dataOutputStream.close(); 57 | return getResponse(connection); 58 | } 59 | 60 | /** 61 | * Executes a get request against the resource provided. 62 | * @param resource the URL to the resource (omitting the host portion) 63 | * @return JSON blob representing the response from the server. 64 | * @throws IOException if establishing the web connection fails 65 | */ 66 | public WebResponse get(String resource) throws IOException { 67 | return get(resource, null); 68 | } 69 | 70 | /** 71 | * Executes a get request against the resource provided. 72 | * @param resource the URL to the resource (omitting the host portion) 73 | * @param queryParameters a map of keys and values to include in the get. 74 | * @return JSON blob representing the response from the server. 75 | * @throws IOException if establishing the web connection fails 76 | */ 77 | public WebResponse get(String resource, Map queryParameters) throws IOException { 78 | String encodedParameterString = mapToQueryParameters(queryParameters); 79 | URLConnection connection = getConnection(GET, resource, encodedParameterString); 80 | return getResponse(connection); 81 | } 82 | 83 | /** 84 | * Returns a string that represents the query parameter component of the URL string. 85 | * Encodes all values using UTF-8 URL encoding. 86 | * @param queryParametersMap a map of keys and values for query parameters 87 | * @return a string of form "key1=value1&key2=value2" 88 | * @throws UnsupportedEncodingException when/if the url encoding to UTF8 fails. 89 | */ 90 | private String mapToQueryParameters(Map queryParametersMap) throws UnsupportedEncodingException { 91 | Set parameterKeyValuePairs = new HashSet(); 92 | if (queryParametersMap != null && !queryParametersMap.isEmpty()) { 93 | for (Map.Entry entry : queryParametersMap.entrySet()) { 94 | String encodedValue = URLEncoder.encode(entry.getValue(), UTF8); 95 | String kvp = String.format("%s=%s", entry.getKey(), encodedValue); 96 | parameterKeyValuePairs.add(kvp); 97 | } 98 | } 99 | return StringUtils.join(parameterKeyValuePairs, "&"); 100 | } 101 | 102 | /** 103 | * Creates and returns a new URLConnection object using the given information. 104 | * @param method GET or POST 105 | * @param endpoint the resource endpoint to connect to 106 | * @param queryParameters query parameters string to use in GET requests 107 | * @return the URLConnection (may be HTTP or HTTPS) 108 | * @throws MalformedURLException if the supplied url is not a valid url 109 | * @throws ProtocolException if the supplied url is not http or https 110 | * @throws IOException if there is a failure establishing an http connection 111 | * @throws IllegalArgumentException if the provided method is not GET or POST 112 | */ 113 | private URLConnection getConnection(String method, String endpoint, String queryParameters) 114 | throws MalformedURLException, ProtocolException, IOException, IllegalArgumentException { 115 | if (!GET.equals(method) && !POST.equals(method)) { 116 | throw new IllegalArgumentException(String.format("Unsupported method '%s'.", method)); 117 | } 118 | 119 | String joinedUrl = StringUtils.join(new String[] {hostUrl, endpoint}, "/"); 120 | if (GET.equals(method) && queryParameters != null && !queryParameters.isEmpty()) 121 | { 122 | joinedUrl = StringUtils.join(new String[]{joinedUrl, queryParameters}, "?"); 123 | } 124 | URL url = new URL(joinedUrl); 125 | URLConnection connection = url.openConnection(); 126 | if (connection instanceof HttpURLConnection) { 127 | ((HttpURLConnection)connection).setRequestMethod(method); 128 | } 129 | connection.setRequestProperty(OCTOPUS_API_KEY_HEADER, apiKey); 130 | return connection; 131 | } 132 | 133 | /** 134 | * Use the connection to read a response from the server. 135 | * @param connection an instantiated URLConnection object. 136 | * @return JSON blob representing the response from the server. 137 | * @throws IOException if there is an issue when connecting or reading the response 138 | * @throws IllegalArgumentException if the connection is null 139 | */ 140 | private WebResponse getResponse(URLConnection connection) throws IOException, IllegalArgumentException { 141 | int responseCode = -1; 142 | if (connection == null) 143 | { 144 | throw new IllegalArgumentException("Connection can not be null when getting a response from server."); 145 | } 146 | connection.connect(); 147 | InputStream streamToRead = null; 148 | if(connection instanceof HttpURLConnection) { 149 | responseCode = ((HttpURLConnection)connection).getResponseCode(); 150 | if (isErrorCode(responseCode)) 151 | { 152 | streamToRead = ((HttpURLConnection)connection).getErrorStream(); 153 | } 154 | } 155 | if (streamToRead == null) { 156 | streamToRead = connection.getInputStream(); 157 | } 158 | BufferedReader reader = new BufferedReader(new InputStreamReader(streamToRead, Charset.forName(UTF8))); 159 | String inputLine; 160 | StringBuilder response = new StringBuilder(); 161 | 162 | while ((inputLine = reader.readLine()) != null) { 163 | response.append(inputLine); 164 | } 165 | reader.close(); 166 | if (connection instanceof HttpURLConnection) { 167 | ((HttpURLConnection)connection).disconnect(); 168 | } 169 | return new WebResponse(responseCode, response.toString()); 170 | } 171 | 172 | 173 | /** 174 | * Returns true if the HTTP Response code represents an error. 175 | * @param code the HTTP Response code 176 | * @return true or false 177 | */ 178 | public final static boolean isErrorCode(final int code) { 179 | return code >= 400; 180 | } 181 | 182 | /** 183 | * A web response code (HTTP Response code) and content from the web request. 184 | */ 185 | public static class WebResponse { 186 | private final int code; 187 | /** 188 | * The HTTP response code. 189 | * @return The HTTP response code. Ex. 200 or 403 190 | */ 191 | public int getCode() { 192 | return code; 193 | } 194 | 195 | /** 196 | * Returns true if the HTTP Response code represents an error. 197 | * @return true or false 198 | */ 199 | public boolean isErrorCode() { 200 | return AuthenticatedWebClient.isErrorCode(code); 201 | } 202 | 203 | private final String content; 204 | /** 205 | * Content for the web response, if any. 206 | * @return JSON content for the web response, if any. 207 | */ 208 | public String getContent() { 209 | return content; 210 | } 211 | 212 | private WebResponse(int code, String content) { 213 | this.code = code; 214 | this.content = content; 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/ChannelsApi.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api; 2 | 3 | import com.octopusdeploy.api.data.Channel; 4 | import java.io.IOException; 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | import net.sf.json.JSONObject; 8 | import net.sf.json.JSONSerializer; 9 | 10 | /** 11 | * Methods for the Channels aspect of the Octopus API 12 | */ 13 | public class ChannelsApi { 14 | private final static String UTF8 = "UTF-8"; 15 | private final AuthenticatedWebClient webClient; 16 | 17 | public ChannelsApi(AuthenticatedWebClient webClient) { 18 | this.webClient = webClient; 19 | } 20 | 21 | /** 22 | * Uses the authenticated web client to pull all channels for a given project 23 | * from the api and convert them to POJOs 24 | * @param projectId the project to get channels for 25 | * @return a Set of Channels (should have at minimum one entry) 26 | * @throws IllegalArgumentException when the web client receives a bad parameter 27 | * @throws IOException When the AuthenticatedWebClient receives and error response code 28 | */ 29 | public Set getChannelsByProjectId(String projectId) throws IllegalArgumentException, IOException { 30 | HashSet channels = new HashSet(); 31 | AuthenticatedWebClient.WebResponse response = webClient.get("api/projects/" + projectId + "/channels"); 32 | if (response.isErrorCode()) { 33 | throw new IOException(String.format("Code %s - %n%s", response.getCode(), response.getContent())); 34 | } 35 | JSONObject json = (JSONObject)JSONSerializer.toJSON(response.getContent()); 36 | for (Object obj : json.getJSONArray("Items")) { 37 | JSONObject jsonObj = (JSONObject)obj; 38 | String id = jsonObj.getString("Id"); 39 | String name = jsonObj.getString("Name"); 40 | String description = jsonObj.getString("Description"); 41 | boolean isDefault = jsonObj.getBoolean("IsDefault"); 42 | channels.add(new Channel(id, name, description, projectId, isDefault)); 43 | } 44 | return channels; 45 | } 46 | 47 | /** 48 | * Uses the authenticated web client to pull a channel by name from a given project 49 | * from the api and convert them to POJOs 50 | * @param projectId the project to get channels for 51 | * @param channelName the channel to return 52 | * @return the named channel for the given project 53 | * @throws IllegalArgumentException when the web client receives a bad parameter 54 | * @throws IOException When the AuthenticatedWebClient receives and error response code 55 | */ 56 | public Channel getChannelByName(String projectId, String channelName) throws IllegalArgumentException, IOException { 57 | AuthenticatedWebClient.WebResponse response = webClient.get("api/projects/" + projectId + "/channels"); 58 | if (response.isErrorCode()) { 59 | throw new IOException(String.format("Code %s - %n%s", response.getCode(), response.getContent())); 60 | } 61 | JSONObject json = (JSONObject)JSONSerializer.toJSON(response.getContent()); 62 | for (Object obj : json.getJSONArray("Items")) { 63 | JSONObject jsonObj = (JSONObject)obj; 64 | String id = jsonObj.getString("Id"); 65 | String name = jsonObj.getString("Name"); 66 | String description = jsonObj.getString("Description"); 67 | boolean isDefault = jsonObj.getBoolean("IsDefault"); 68 | if (channelName.equals(name)) 69 | { 70 | return new Channel(id, name, description, projectId, isDefault); 71 | } 72 | } 73 | return null; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/DeploymentsApi.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api; 2 | 3 | import com.octopusdeploy.api.data.DeploymentProcess; 4 | import com.octopusdeploy.api.data.DeploymentProcessStep; 5 | import com.octopusdeploy.api.data.DeploymentProcessStepAction; 6 | import com.octopusdeploy.api.data.DeploymentProcessTemplate; 7 | import com.octopusdeploy.api.data.SelectedPackage; 8 | import com.octopusdeploy.api.data.Variable; 9 | import java.io.IOException; 10 | import java.nio.charset.Charset; 11 | import java.util.HashMap; 12 | import java.util.HashSet; 13 | import java.util.Set; 14 | import net.sf.json.JSONArray; 15 | import net.sf.json.JSONObject; 16 | import net.sf.json.JSONSerializer; 17 | import org.apache.commons.lang.StringUtils; 18 | 19 | public class DeploymentsApi { 20 | private final static String UTF8 = "UTF-8"; 21 | private final AuthenticatedWebClient webClient; 22 | 23 | public DeploymentsApi(AuthenticatedWebClient webClient) { 24 | this.webClient = webClient; 25 | } 26 | 27 | /** 28 | * Deploys a given release to provided environment. 29 | * @param releaseId Release Id from Octopus to deploy. 30 | * @param environmentId Environment Id from Octopus to deploy to. 31 | * @param tenantId Tenant Id from Octopus to deploy to. 32 | * @return the content of the web response. 33 | * @throws IOException When the AuthenticatedWebClient receives and error response code 34 | */ 35 | public String executeDeployment(String releaseId, String environmentId, String tenantId) throws IOException { 36 | return executeDeployment( releaseId, environmentId, tenantId, null); 37 | } 38 | 39 | /** 40 | * Deploys a given release to provided environment. 41 | * @param releaseId Release Id from Octopus to deploy. 42 | * @param environmentId Environment Id from Octopus to deploy to. 43 | * @param tenantId Tenant Id from Octopus to deploy to. 44 | * @param variables Variables used during deployment. 45 | * @return the content of the web response. 46 | * @throws IOException When the AuthenticatedWebClient receives and error response code 47 | */ 48 | public String executeDeployment(String releaseId, String environmentId, String tenantId, Set variables) throws IOException { 49 | StringBuilder jsonBuilder = new StringBuilder(); 50 | jsonBuilder.append(String.format("{EnvironmentId:\"%s\",ReleaseId:\"%s\"", environmentId, releaseId)); 51 | 52 | if (tenantId != null && !tenantId.isEmpty()) { 53 | jsonBuilder.append(String.format(",TenantId:\"%s\"", tenantId)); 54 | } 55 | if (variables != null && !variables.isEmpty()) { 56 | jsonBuilder.append(",FormValues:{"); 57 | Set variablesStrings = new HashSet(); 58 | for (Variable v : variables) { 59 | variablesStrings.add(String.format("\"%s\":\"%s\"", v.getId(), v.getValue())); 60 | } 61 | jsonBuilder.append(StringUtils.join(variablesStrings, ",")); 62 | jsonBuilder.append("}"); 63 | } 64 | jsonBuilder.append("}"); 65 | String json = jsonBuilder.toString(); 66 | 67 | byte[] data = json.getBytes(Charset.forName(UTF8)); 68 | AuthenticatedWebClient.WebResponse response = webClient.post("api/deployments", data); 69 | if (response.isErrorCode()) { 70 | String errorMsg = ErrorParser.getErrorsFromResponse(response.getContent()); 71 | throw new IOException(String.format("Code %s - %n%s", response.getCode(), errorMsg)); 72 | } 73 | return response.getContent(); 74 | } 75 | 76 | /** 77 | * Return a representation of a deployment process for a given project. 78 | * @param projectId the id of the project to get the process for. 79 | * @return DeploymentProcess a representation of the process 80 | * @throws IllegalArgumentException when the web client receives a bad parameter 81 | * @throws IOException When the AuthenticatedWebClient receives and error response code 82 | */ 83 | public DeploymentProcess getDeploymentProcessForProject(String projectId) throws IllegalArgumentException, IOException { 84 | // TODO: refactor/method extract/clean up 85 | AuthenticatedWebClient.WebResponse response = webClient.get("api/deploymentprocesses/deploymentprocess-" + projectId); 86 | if (response.isErrorCode()) { 87 | throw new IOException(String.format("Code %s - %n%s", response.getCode(), response.getContent())); 88 | } 89 | JSONObject json = (JSONObject)JSONSerializer.toJSON(response.getContent()); 90 | JSONArray stepsJson = json.getJSONArray("Steps"); 91 | HashSet deploymentProcessSteps = new HashSet(); 92 | for (Object stepObj : stepsJson) { 93 | JSONObject jsonStepObj = (JSONObject)stepObj; 94 | HashSet deploymentProcessStepActions = new HashSet(); 95 | 96 | JSONArray actionsJson = jsonStepObj.getJSONArray("Actions"); 97 | for (Object actionObj : actionsJson) { 98 | JSONObject jsonActionObj = (JSONObject)actionObj; 99 | JSONObject propertiesJson = jsonActionObj.getJSONObject("Properties"); 100 | HashMap properties = new HashMap(); 101 | for (Object key : propertiesJson.keySet()) { 102 | String keyString = key.toString(); 103 | properties.put(keyString, propertiesJson.getString(keyString)); 104 | } 105 | String dpsaId = jsonActionObj.getString("Id"); 106 | String dpsaName = jsonActionObj.getString("Name"); 107 | String dpsaType = jsonActionObj.getString("ActionType"); 108 | deploymentProcessStepActions.add(new DeploymentProcessStepAction(dpsaId, dpsaName, dpsaType, properties)); 109 | } 110 | String dpsId = jsonStepObj.getString("Id"); 111 | String dpsName = jsonStepObj.getString("Name"); 112 | deploymentProcessSteps.add(new DeploymentProcessStep(dpsId, dpsName, deploymentProcessStepActions)); 113 | } 114 | String dpId = json.getString("Id"); 115 | String dpProject = json.getString("ProjectId"); 116 | return new DeploymentProcess(dpId, dpProject, deploymentProcessSteps); 117 | } 118 | 119 | /** 120 | * Return a representation of a deployment process for a given project. 121 | * @param projectId project id 122 | * @return DeploymentProcessTemplate deployment process template 123 | * @throws IllegalArgumentException when the web client receives a bad parameter 124 | * @throws IOException When the AuthenticatedWebClient receives and error response code 125 | */ 126 | public DeploymentProcessTemplate getDeploymentProcessTemplateForProject(String projectId) throws IllegalArgumentException, IOException { 127 | AuthenticatedWebClient.WebResponse response = webClient.get("api/deploymentprocesses/deploymentprocess-" + projectId + "/template"); 128 | if (response.isErrorCode()) { 129 | throw new IOException(String.format("Code %s - %n%s", response.getCode(), response.getContent())); 130 | } 131 | 132 | JSONObject json = (JSONObject)JSONSerializer.toJSON(response.getContent()); 133 | Set packages = new HashSet(); 134 | String deploymentId = json.getString("DeploymentProcessId"); 135 | JSONArray pkgsJson = json.getJSONArray("Packages"); 136 | for (Object pkgObj : pkgsJson) { 137 | JSONObject pkgJsonObj = (JSONObject) pkgObj; 138 | String name = pkgJsonObj.getString("StepName"); 139 | String packageId = pkgJsonObj.getString("PackageId"); 140 | String packageReferenceName = pkgJsonObj.getString("PackageReferenceName"); 141 | String version = pkgJsonObj.getString("VersionSelectedLastRelease"); 142 | packages.add(new SelectedPackage(name, packageId, packageReferenceName, version)); 143 | } 144 | 145 | DeploymentProcessTemplate template = new DeploymentProcessTemplate(deploymentId, projectId, packages); 146 | return template; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/EnvironmentsApi.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api; 2 | 3 | import com.octopusdeploy.api.data.Environment; 4 | import java.io.IOException; 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | import net.sf.json.JSONArray; 8 | import net.sf.json.JSONObject; 9 | import net.sf.json.JSONSerializer; 10 | 11 | public class EnvironmentsApi { 12 | private final static String UTF8 = "UTF-8"; 13 | private final AuthenticatedWebClient webClient; 14 | 15 | public EnvironmentsApi(AuthenticatedWebClient webClient) { 16 | this.webClient = webClient; 17 | } 18 | 19 | /** 20 | * Get all environments from the Octopus server as Environment objects. 21 | * @return A set of all environments on the Octopus server. 22 | * @throws IllegalArgumentException when the web client receives a bad parameter 23 | * @throws IOException When the AuthenticatedWebClient receives and error response code 24 | */ 25 | public Set getAllEnvironments() throws IllegalArgumentException, IOException { 26 | HashSet environments = new HashSet(); 27 | AuthenticatedWebClient.WebResponse response =webClient.get("api/environments/all"); 28 | if (response.isErrorCode()) { 29 | throw new IOException(String.format("Code %s - %n%s", response.getCode(), response.getContent())); 30 | } 31 | JSONArray json = (JSONArray)JSONSerializer.toJSON(response.getContent()); 32 | for (Object obj : json) { 33 | JSONObject jsonObj = (JSONObject)obj; 34 | String id = jsonObj.getString("Id"); 35 | String name = jsonObj.getString("Name"); 36 | String description = jsonObj.getString("Description"); 37 | environments.add(new Environment(id, name, description)); 38 | } 39 | return environments; 40 | } 41 | 42 | /** 43 | * Get the Environment with the given name if it exists, return null otherwise. 44 | * Only selects the environment if the name is an exact match (including case) 45 | * @param name The name of the Environment to find. 46 | * @return The Environment with that name. 47 | * @throws IllegalArgumentException when the web client receives a bad parameter 48 | * @throws IOException When the AuthenticatedWebClient receives and error response code 49 | */ 50 | public Environment getEnvironmentByName(String name) throws IllegalArgumentException, IOException { 51 | return getEnvironmentByName(name, false); 52 | } 53 | 54 | /** 55 | * Get the Environment with the given name if it exists, return null otherwise. 56 | * @param name The name of the Environment to find. 57 | * @param ignoreCase when true uses equalsIgnoreCase in the name check 58 | * @return The Environment with that name. 59 | * @throws IllegalArgumentException when the web client receives a bad parameter 60 | * @throws IOException When the AuthenticatedWebClient receives and error response code 61 | */ 62 | public Environment getEnvironmentByName(String name, boolean ignoreCase) throws IllegalArgumentException, IOException { 63 | Set environments = getAllEnvironments(); 64 | for (Environment env : environments) { 65 | if ((ignoreCase && name.equalsIgnoreCase(env.getName())) || 66 | (!ignoreCase && name.equals(env.getName()))) { 67 | return env; 68 | } 69 | } 70 | return null; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/ErrorParser.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | import org.apache.commons.lang.StringEscapeUtils; 9 | 10 | /** 11 | * Parses errors from Octopus html/javascript responses 12 | * @author jlabroad 13 | */ 14 | public class ErrorParser { 15 | 16 | /** Find a group of messages in the format: "Errors":["error message 1", "error message 2", "error message 3"] */ 17 | protected static final String errDetailsOutsideString = "(?:\\\"Errors\\\")(?:[^\\[]\\[)(?[^\\]]+)"; 18 | protected static final Pattern errDetailsOutsidePattern = Pattern.compile(errDetailsOutsideString); 19 | 20 | /** Parse each individual message from "error message 1", "error message 2", "error message 3" */ 21 | protected static final String errDetailsInsideString = "(?:\\\")(?[^\\\"]+)*(?:\\\")"; 22 | protected static final Pattern errDetailsInsidePattern = Pattern.compile(errDetailsInsideString); 23 | 24 | /** 25 | * Parse any errors from the returned HTML/javascript from Octopus 26 | * @param response The Octopus html response that may include error data 27 | * @return A list of error strings 28 | */ 29 | public static String getErrorsFromResponse(String response) { 30 | List errorStrings = new ArrayList(); 31 | 32 | //Get the error title and main message 33 | String errorTitle = getErrorDataByFieldName("title", response); 34 | if (!errorTitle.isEmpty()) { 35 | errorStrings.add(String.format("%s", errorTitle)); 36 | } 37 | 38 | //Get the error details 39 | String errorDetailMessage = getErrorDataByFieldName("ErrorMessage", response); 40 | if (!errorDetailMessage.isEmpty()) { 41 | errorStrings.add("\t" + errorDetailMessage); 42 | } 43 | errorStrings.addAll(getErrorDetails(response)); 44 | 45 | StringBuilder errorMsg = new StringBuilder(); 46 | for (String err : errorStrings) { 47 | errorMsg.append(String.format("%s%n", err)); 48 | } 49 | 50 | return errorMsg.toString(); 51 | } 52 | 53 | /** 54 | * Grabs a single error data field from an Octopus html response 55 | * @param fieldName The field name of the error string 56 | * @param response The field data 57 | * @return The error data 58 | */ 59 | protected static String getErrorDataByFieldName(String fieldName, String response) { 60 | //Get the next string in script parameter list: "var errorData = {:"Field value", ... 61 | final String patternString = String.format("(?:errorData.+)(?:\"%s\")(?:[:\\[\"]+)(?[^\"]+)", fieldName); 62 | 63 | Pattern pattern = Pattern.compile(patternString); 64 | Matcher matcher = pattern.matcher(response); 65 | String errData = ""; 66 | if (matcher.find() && matcher.groupCount() > 0) { 67 | errData = matcher.group("fieldValue"); 68 | } 69 | return errData; 70 | } 71 | 72 | /** 73 | * Returns a list of "Errors" values from Octopus html response 74 | * @param response The full Octopus html response 75 | * @return a list of error details 76 | */ 77 | protected static List getErrorDetails(String response) { 78 | List errorList = new ArrayList(); 79 | 80 | Matcher m = errDetailsOutsidePattern.matcher(response); 81 | if (m.find() && m.groupCount() > 0) { 82 | //Split up the list of error messages into individual messages 83 | String errors = m.group("fullDetailString"); 84 | m = errDetailsInsidePattern.matcher(errors); 85 | while (m.find() && m.groupCount() > 0) { 86 | String singleError = StringEscapeUtils.unescapeJava(m.group("singleError")); 87 | errorList.add("\t" + singleError); 88 | } 89 | } 90 | return errorList; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/OctopusApi.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api; 2 | 3 | public class OctopusApi { 4 | private final AuthenticatedWebClient webClient; 5 | 6 | private final ChannelsApi channelsApi; 7 | public ChannelsApi getChannelsApi() { 8 | return channelsApi; 9 | } 10 | 11 | private final TenantsApi tenantsApi; 12 | public TenantsApi getTenantsApi() { 13 | return tenantsApi; 14 | } 15 | 16 | private final EnvironmentsApi environmentsApi; 17 | public EnvironmentsApi getEnvironmentsApi() { 18 | return environmentsApi; 19 | } 20 | 21 | private final ProjectsApi projectsApi; 22 | public ProjectsApi getProjectsApi() { 23 | return projectsApi; 24 | } 25 | 26 | private final DeploymentsApi deploymentsApi; 27 | public DeploymentsApi getDeploymentsApi() { 28 | return deploymentsApi; 29 | } 30 | 31 | private final ReleasesApi releasesApi; 32 | public ReleasesApi getReleasesApi() { 33 | return releasesApi; 34 | } 35 | 36 | private final VariablesApi variablesApi; 37 | public VariablesApi getVariablesApi() { 38 | return variablesApi; 39 | } 40 | 41 | private final TasksApi tasksApi; 42 | public TasksApi getTasksApi() { 43 | return tasksApi; 44 | } 45 | 46 | public OctopusApi(String octopusHost, String apiKey) { 47 | webClient = new AuthenticatedWebClient(octopusHost, apiKey); 48 | channelsApi = new ChannelsApi(webClient); 49 | tenantsApi = new TenantsApi(webClient); 50 | environmentsApi = new EnvironmentsApi(webClient); 51 | projectsApi = new ProjectsApi(webClient); 52 | deploymentsApi = new DeploymentsApi(webClient); 53 | releasesApi = new ReleasesApi(webClient); 54 | variablesApi = new VariablesApi(webClient); 55 | tasksApi = new TasksApi(webClient); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/ProjectsApi.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api; 2 | 3 | import com.octopusdeploy.api.data.Project; 4 | import java.io.IOException; 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | import net.sf.json.JSONArray; 8 | import net.sf.json.JSONObject; 9 | import net.sf.json.JSONSerializer; 10 | 11 | public class ProjectsApi { 12 | private final AuthenticatedWebClient webClient; 13 | 14 | public ProjectsApi(AuthenticatedWebClient webClient) { 15 | this.webClient = webClient; 16 | } 17 | 18 | /** 19 | * Uses the authenticated web client to pull all projects from the api and 20 | * convert them to POJOs 21 | * @return a Set of Projects (may be empty) 22 | * @throws IllegalArgumentException when the web client receives a bad parameter 23 | * @throws IOException When the AuthenticatedWebClient receives and error response code 24 | */ 25 | public Set getAllProjects() throws IllegalArgumentException, IOException { 26 | HashSet projects = new HashSet(); 27 | AuthenticatedWebClient.WebResponse response = webClient.get("api/projects/all"); 28 | if (response.isErrorCode()) { 29 | throw new IOException(String.format("Code %s - %n%s", response.getCode(), response.getContent())); 30 | } 31 | JSONArray json = (JSONArray)JSONSerializer.toJSON(response.getContent()); 32 | for (Object obj : json) { 33 | JSONObject jsonObj = (JSONObject)obj; 34 | String id = jsonObj.getString("Id"); 35 | String name = jsonObj.getString("Name"); 36 | projects.add(new Project(id, name)); 37 | } 38 | return projects; 39 | } 40 | 41 | /** 42 | * Loads in the full list of projects from the API, then selects one project by name. 43 | * Only selects the project if the name is an exact match (including case) 44 | * @param name name of the project to select 45 | * @return the named project or null if no such project exists 46 | * @throws IllegalArgumentException when the web client receives a bad parameter 47 | * @throws IOException When the AuthenticatedWebClient receives and error response code 48 | */ 49 | public Project getProjectByName(String name) throws IllegalArgumentException, IOException { 50 | return getProjectByName(name, false); 51 | } 52 | 53 | /** 54 | * Loads in the full list of projects from the API, then selects one project by name. 55 | * @param name name of the project to select 56 | * @param ignoreCase when true uses equalsIgnoreCase in the name check 57 | * @return the named project or null if no such project exists 58 | * @throws IllegalArgumentException when the web client receives a bad parameter 59 | * @throws IOException When the AuthenticatedWebClient receives and error response code 60 | */ 61 | public Project getProjectByName(String name, boolean ignoreCase) throws IllegalArgumentException, IOException { 62 | Set allProjects = getAllProjects(); 63 | for (Project project : allProjects) { 64 | if ((ignoreCase && name.equalsIgnoreCase(project.getName())) || 65 | (!ignoreCase && name.equals(project.getName()))) { 66 | return project; 67 | } 68 | } 69 | return null; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/ReleasesApi.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api; 2 | 3 | import com.octopusdeploy.api.data.Release; 4 | import com.octopusdeploy.api.data.SelectedPackage; 5 | import java.io.IOException; 6 | import java.nio.charset.Charset; 7 | import java.util.HashSet; 8 | import java.util.Set; 9 | import net.sf.json.JSONObject; 10 | import net.sf.json.JSONSerializer; 11 | import org.apache.commons.lang.StringUtils; 12 | 13 | public class ReleasesApi { 14 | private final static String UTF8 = "UTF-8"; 15 | private final AuthenticatedWebClient webClient; 16 | 17 | public ReleasesApi(AuthenticatedWebClient webClient) { 18 | this.webClient = webClient; 19 | } 20 | 21 | /** 22 | * Creates a release in octopus deploy. 23 | * @param project The project id 24 | * @param releaseVersion The version number for this release. 25 | * @return content from the API post 26 | * @throws java.io.IOException When the AuthenticatedWebClient receives and error response code 27 | */ 28 | public String createRelease(String project, String releaseVersion) throws IOException { 29 | return createRelease(project, releaseVersion, null); 30 | } 31 | 32 | /** 33 | * Creates a release in octopus deploy. 34 | * @param project The project id. 35 | * @param releaseVersion The version number for this release. 36 | * @param releaseNotes Release notes to be associated with this release. 37 | * @return content from the API post 38 | * @throws java.io.IOException When the AuthenticatedWebClient receives and error response code 39 | */ 40 | public String createRelease(String project, String releaseVersion, String releaseNotes) throws IOException { 41 | return createRelease(project, releaseVersion, null, releaseNotes, null); 42 | } 43 | 44 | /** 45 | * Creates a release in octopus deploy. 46 | * @param project The project id 47 | * @param releaseVersion The version number for this release. 48 | * @param channelId The channel to create the release on. 49 | * @param releaseNotes Release notes to be associated with this release. 50 | * @param selectedPackages Packages to be deployed with this release. 51 | * @return content from the API post 52 | * @throws java.io.IOException When the AuthenticatedWebClient receives and error response code 53 | */ 54 | public String createRelease(String project, String releaseVersion, String channelId, String releaseNotes, Set selectedPackages) throws IOException { 55 | StringBuilder jsonBuilder = new StringBuilder(); 56 | jsonBuilder.append(String.format("{ProjectId:\"%s\",Version:\"%s\"", project, releaseVersion)); 57 | if (channelId != null && !channelId.isEmpty()) { 58 | jsonBuilder.append(String.format(",ChannelId:\"%s\"", channelId)); 59 | } 60 | if (releaseNotes != null && !releaseNotes.isEmpty()) { 61 | jsonBuilder.append(String.format(",ReleaseNotes:\"%s\"", releaseNotes)); 62 | } 63 | if (selectedPackages != null && !selectedPackages.isEmpty()) { 64 | jsonBuilder.append(",SelectedPackages:["); 65 | Set selectedPackageStrings = new HashSet(); 66 | for (SelectedPackage selectedPackage : selectedPackages) { 67 | // StepName has been deprecated, ActionName should now be used. Continue passing StepName in case an older 68 | // version of Octopus server is in use. 69 | String actionName = selectedPackage.getStepName(); 70 | selectedPackageStrings.add(String.format("{StepName:\"%s\",ActionName:\"%s\",PackageReferenceName:\"%s\",Version:\"%s\"}", actionName, actionName, selectedPackage.getPackageReferenceName(), selectedPackage.getVersion())); 71 | } 72 | jsonBuilder.append(StringUtils.join(selectedPackageStrings, ",")); 73 | jsonBuilder.append("]"); 74 | } 75 | jsonBuilder.append("}"); 76 | String json = jsonBuilder.toString(); 77 | byte[] data = json.getBytes(Charset.forName(UTF8)); 78 | AuthenticatedWebClient.WebResponse response = webClient.post("api/releases", data); 79 | if (response.isErrorCode()) { 80 | String errorMsg = ErrorParser.getErrorsFromResponse(response.getContent()); 81 | throw new IOException(String.format("Code %s - %n%s", response.getCode(), errorMsg)); 82 | } 83 | return response.getContent(); 84 | } 85 | 86 | /** 87 | * Get all releases for a given project from the Octopus server; 88 | * @param projectId the id of the project to get the releases for 89 | * @return A set of all releases for a given project 90 | * @throws IllegalArgumentException when the web client receives a bad parameter 91 | * @throws IOException When the AuthenticatedWebClient receives and error response code 92 | */ 93 | public Set getReleasesForProject(String projectId) throws IllegalArgumentException, IOException { 94 | HashSet releases = new HashSet(); 95 | AuthenticatedWebClient.WebResponse response = webClient.get("api/projects/" + projectId + "/releases"); 96 | if (response.isErrorCode()) { 97 | throw new IOException(String.format("Code %s - %n%s", response.getCode(), response.getContent())); 98 | } 99 | JSONObject json = (JSONObject)JSONSerializer.toJSON(response.getContent()); 100 | for (Object obj : json.getJSONArray("Items")) { 101 | JSONObject jsonObj = (JSONObject)obj; 102 | String id = jsonObj.getString("Id"); 103 | String version = jsonObj.getString("Version"); 104 | String channelId = jsonObj.getString("ChannelId"); 105 | String ReleaseNotes = jsonObj.getString("ReleaseNotes"); 106 | releases.add(new Release(id, projectId, channelId, ReleaseNotes, version)); 107 | } 108 | return releases; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/TasksApi.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api; 2 | 3 | import com.octopusdeploy.api.data.Task; 4 | import java.io.IOException; 5 | import net.sf.json.JSONObject; 6 | import net.sf.json.JSONSerializer; 7 | 8 | public class TasksApi { 9 | private final AuthenticatedWebClient webClient; 10 | 11 | public TasksApi(AuthenticatedWebClient webClient) { 12 | this.webClient = webClient; 13 | } 14 | 15 | /** 16 | * Retrieves a task by its id. 17 | * @param taskId task id 18 | * @return a Task object 19 | * @throws IllegalArgumentException when the web client receives a bad parameter 20 | * @throws IOException When the AuthenticatedWebClient receives and error response code 21 | */ 22 | public Task getTask(String taskId) throws IllegalArgumentException, IOException { 23 | AuthenticatedWebClient.WebResponse response = webClient.get("api/tasks/" + taskId); 24 | if (response.isErrorCode()) { 25 | throw new IOException(String.format("Code %s - %n%s", response.getCode(), response.getContent())); 26 | } 27 | JSONObject json = (JSONObject)JSONSerializer.toJSON(response.getContent()); 28 | String id = json.getString("Id"); 29 | String name = json.getString("Name"); 30 | String description = json.getString("Description"); 31 | String state = json.getString("State"); 32 | boolean isCompleted = json.getBoolean("IsCompleted"); 33 | return new Task(id, name, description, state, isCompleted); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/TenantsApi.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api; 2 | 3 | import com.octopusdeploy.api.data.Tenant; 4 | import java.io.IOException; 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | import net.sf.json.JSONArray; 8 | import net.sf.json.JSONObject; 9 | import net.sf.json.JSONSerializer; 10 | 11 | /** 12 | * Methods for the Tenants aspects of the Octopus API 13 | */ 14 | public class TenantsApi { 15 | private final AuthenticatedWebClient webClient; 16 | 17 | public TenantsApi(AuthenticatedWebClient webClient) { 18 | this.webClient = webClient; 19 | } 20 | 21 | /** 22 | * Uses the authenticated web client to pull all tenants from the api and 23 | * convert them to POJOs 24 | * @return a Set of Tenants (may be empty) 25 | * @throws IllegalArgumentException when the web client receives a bad parameter 26 | * @throws IOException When the AuthenticatedWebClient receives and error response code 27 | */ 28 | public Set getAllTenants() throws IllegalArgumentException, IOException { 29 | HashSet tenants = new HashSet(); 30 | AuthenticatedWebClient.WebResponse response = webClient.get("api/tenants/all"); 31 | if (response.isErrorCode()) { 32 | throw new IOException(String.format("Code %s - %n%s", response.getCode(), response.getContent())); 33 | } 34 | JSONArray json = (JSONArray)JSONSerializer.toJSON(response.getContent()); 35 | for (Object obj : json) { 36 | JSONObject jsonObj = (JSONObject)obj; 37 | String id = jsonObj.getString("Id"); 38 | String name = jsonObj.getString("Name"); 39 | tenants.add(new Tenant(id, name)); 40 | } 41 | return tenants; 42 | } 43 | 44 | /** 45 | * Get the Tenant with the given name if it exists, return null otherwise. 46 | * Only selects the tenant if the name is an exact match (including case) 47 | * @param name The name of the Tenant to find. 48 | * @return The Tenant with that name. 49 | * @throws IllegalArgumentException when the web client receives a bad parameter 50 | * @throws IOException When the AuthenticatedWebClient receives and error response code 51 | */ 52 | public Tenant getTenantByName(String name) throws IllegalArgumentException, IOException { 53 | return getTenantByName(name, false); 54 | } 55 | 56 | /** 57 | * Get the Tenant with the given name if it exists, return null otherwise. 58 | * @param name The name of the Tenant to find. 59 | * @param ignoreCase when true uses equalsIgnoreCase in the name check 60 | * @return The Environment with that name. 61 | * @throws IllegalArgumentException when the web client receives a bad parameter 62 | * @throws IOException When the AuthenticatedWebClient receives and error response code 63 | */ 64 | public Tenant getTenantByName(String name, boolean ignoreCase) throws IllegalArgumentException, IOException { 65 | Set tenants = getAllTenants(); 66 | for (Tenant tenant : tenants) { 67 | if ((ignoreCase && name.equalsIgnoreCase(tenant.getName())) || 68 | (!ignoreCase && name.equals(tenant.getName()))) { 69 | return tenant; 70 | } 71 | } 72 | return null; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/VariablesApi.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api; 2 | 3 | import com.octopusdeploy.api.data.Variable; 4 | import java.io.IOException; 5 | import java.util.HashSet; 6 | import java.util.Properties; 7 | import java.util.Set; 8 | import net.sf.json.JSONObject; 9 | import net.sf.json.JSONSerializer; 10 | import org.apache.commons.lang.StringUtils; 11 | 12 | public class VariablesApi { 13 | private final AuthenticatedWebClient webClient; 14 | 15 | public VariablesApi(AuthenticatedWebClient webClient) { 16 | this.webClient = webClient; 17 | } 18 | 19 | /** 20 | * Get the variables for a combination of release and environment, return null otherwise. 21 | * @param releaseId The id of the Release. 22 | * @param environmentId The id of the Environment. 23 | * @param entryProperties entry properties 24 | * @return A set of all variables for a given Release and Environment combination. 25 | * @throws IllegalArgumentException when the web client receives a bad parameter 26 | * @throws IOException When the AuthenticatedWebClient receives and error response code 27 | */ 28 | public Set getVariablesByReleaseAndEnvironment(String releaseId, String environmentId, Properties entryProperties) throws IllegalArgumentException, IOException { 29 | Set variables = new HashSet(); 30 | 31 | AuthenticatedWebClient.WebResponse response = webClient.get("api/releases/" + releaseId + "/deployments/preview/" + environmentId); 32 | if (response.isErrorCode()) { 33 | throw new IOException(String.format("Code %s - %n%s", response.getCode(), response.getContent())); 34 | } 35 | JSONObject json = (JSONObject)JSONSerializer.toJSON(response.getContent()); 36 | JSONObject form = json.getJSONObject("Form"); 37 | if (form != null){ 38 | JSONObject formValues = form.getJSONObject("Values"); 39 | for (Object obj : form.getJSONArray("Elements")) { 40 | JSONObject jsonObj = (JSONObject) obj; 41 | String id = jsonObj.getString("Name"); 42 | String name = jsonObj.getJSONObject("Control").getString("Name"); 43 | String value = formValues.getString(id); 44 | 45 | String entryValue = entryProperties.getProperty(name); 46 | if (StringUtils.isNotEmpty(entryValue)) { 47 | value = entryValue; 48 | } 49 | String description = jsonObj.getJSONObject("Control").getString("Description"); 50 | variables.add(new Variable(id, name, value, description)); 51 | } 52 | } 53 | 54 | return variables; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/data/Channel.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api.data; 2 | 3 | /** 4 | * Simple representation of a Channel in Octopus. 5 | */ 6 | public class Channel { 7 | private final String id; 8 | public String getId() { 9 | return id; 10 | } 11 | 12 | private final String name; 13 | public String getName() { 14 | return name; 15 | } 16 | 17 | private final String description; 18 | public String getDescription() { 19 | return description; 20 | } 21 | 22 | private final String projectId; 23 | public final String getProjectId() { 24 | return projectId; 25 | } 26 | 27 | private final boolean isDefault; 28 | public boolean getIsDefault() { 29 | return isDefault; 30 | } 31 | 32 | public Channel(String id, String name, String description, String projectId, boolean isDefault) { 33 | this.id = id; 34 | this.name = name; 35 | this.description = description; 36 | this.projectId = projectId; 37 | this.isDefault = isDefault; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return String.format("id= %s, name= %s, description= %s, projectId= %s, isDefault= %b", id, name, description, projectId, isDefault); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/data/DeploymentProcess.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api.data; 2 | 3 | import java.util.Set; 4 | 5 | /** 6 | * Deployment process is an Octopus concept that ties a project to the project's individual steps. 7 | */ 8 | public class DeploymentProcess { 9 | private final String id; 10 | public String getId() { 11 | return id; 12 | } 13 | 14 | private final String projectId; 15 | public String getProjectId() { 16 | return projectId; 17 | } 18 | 19 | private final Set steps; 20 | public Set getSteps() { 21 | return steps; 22 | } 23 | 24 | public DeploymentProcess(String id, String projectId, Set steps) { 25 | this.id = id; 26 | this.projectId = projectId; 27 | this.steps = steps; 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return "DeploymentProcess [id=" + id + ", projectId=" + projectId + ", steps=" + steps + "]"; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/data/DeploymentProcessStep.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api.data; 2 | 3 | import java.util.Set; 4 | 5 | /** 6 | * A Project's release deployment process step. 7 | */ 8 | public class DeploymentProcessStep { 9 | private final String id; 10 | public String getId() { 11 | return id; 12 | } 13 | 14 | private final String name; 15 | public String getName() { 16 | return name; 17 | } 18 | 19 | private final Set actions; 20 | public Set getActions() { 21 | return actions; 22 | } 23 | 24 | public DeploymentProcessStep(String id, String name, Set actions) { 25 | this.id = id; 26 | this.name = name; 27 | this.actions = actions; 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return "DeploymentProcessStep [id=" + id + ", name=" + name + ", actions=" + actions + "]"; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/data/DeploymentProcessStepAction.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api.data; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * Actions for Deployment Process Steps. 7 | */ 8 | public class DeploymentProcessStepAction { 9 | private final String id; 10 | public String getId() { 11 | return id; 12 | } 13 | 14 | private final String name; 15 | public String getName() { 16 | return name; 17 | } 18 | 19 | private final String actionType; 20 | public String getActionType() { 21 | return actionType; 22 | } 23 | 24 | private final Map properties; 25 | public Map getProperties() { 26 | return properties; 27 | } 28 | 29 | public DeploymentProcessStepAction(String id, String name, String actionType, Map properties) 30 | { 31 | this.id = id; 32 | this.name = name; 33 | this.actionType = actionType; 34 | this.properties = properties; 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return "DeploymentProcessStepAction [id=" + id + ", name=" + name + ", actionType=" + actionType + ", properties=" + properties + "]"; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/data/DeploymentProcessTemplate.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api.data; 2 | 3 | import java.util.Set; 4 | 5 | /** 6 | * Represents a set of packages from the deployment process template 7 | */ 8 | public class DeploymentProcessTemplate { 9 | private final String id; 10 | public String getId() { 11 | return id; 12 | } 13 | 14 | private final String projectId; 15 | public String getProjectId() { 16 | return projectId; 17 | } 18 | 19 | private final Set packages; 20 | public Set getSteps() { 21 | return packages; 22 | } 23 | 24 | public DeploymentProcessTemplate(String id, String projectId, Set packages) { 25 | this.id = id; 26 | this.projectId = projectId; 27 | this.packages = packages; 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return "DeploymentProcessTemplate [id=" + id + ", projectId=" + projectId + ", packages=" + packages + "]"; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/data/Environment.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api.data; 2 | 3 | /** 4 | * Represents an environment. Environments are user-defined and map to real world 5 | * deployment environments such as development, staging, test and production. 6 | * Projects are deployed to environments. 7 | */ 8 | public class Environment { 9 | private final String name; 10 | public String getName() { 11 | return name; 12 | } 13 | 14 | private final String id; 15 | public String getId() { 16 | return id; 17 | } 18 | 19 | private final String description; 20 | public String getDescription() { 21 | return description; 22 | } 23 | 24 | public Environment(String id, String name, String description) 25 | { 26 | this.id = id; 27 | this.name = name; 28 | this.description = description; 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return "Environment [name=" + name + ", id=" + id + ", description=" + description + "]"; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/data/Project.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api.data; 2 | 3 | /** 4 | * Represents a project from the OctopusDeploy API. 5 | */ 6 | public class Project { 7 | private final String name; 8 | public String getName() { 9 | return name; 10 | } 11 | 12 | private final String id; 13 | public String getId() { 14 | return id; 15 | } 16 | 17 | public Project(String id, String name) { 18 | this.id = id; 19 | this.name = name; 20 | } 21 | 22 | @Override 23 | public String toString() { 24 | return "Project [name=" + name + ", id=" + id + "]"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/data/Release.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api.data; 2 | 3 | /** 4 | * A simplified representation of a Release in OctopusDeploy. 5 | */ 6 | public class Release { 7 | private final String id; 8 | public String getId() { 9 | return id; 10 | } 11 | 12 | private final String projectId; 13 | public String getProjectId() { 14 | return projectId; 15 | } 16 | 17 | private final String channelId; 18 | public String getChannelId() { 19 | return channelId; 20 | } 21 | 22 | private final String releaseNotes; 23 | public String getReleaseNotes() { 24 | return releaseNotes; 25 | } 26 | 27 | private final String version; 28 | public String getVersion() { 29 | return version; 30 | } 31 | 32 | public Release(String id, String projectId, String channelId, String releaseNotes, String version){ 33 | this.id = id; 34 | this.projectId = projectId; 35 | this.channelId = channelId; 36 | this.releaseNotes = releaseNotes; 37 | this.version = version; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return "Release [id=" + id + ", projectId=" + projectId + ", ChannelId=" + channelId + ", releaseNotes=" + releaseNotes + ", version=" + version + "]"; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/data/SelectedPackage.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api.data; 2 | 3 | /** 4 | * Represents a SelectedPackage, part of a Release. 5 | */ 6 | public class SelectedPackage { 7 | 8 | private String stepName; 9 | public String getStepName() { 10 | return stepName; 11 | } 12 | public void setStepName(String stepName) { this.stepName = stepName; } 13 | 14 | private final String packageId; 15 | public String getPackageId() { return packageId; } 16 | 17 | private final String packageReferenceName; 18 | public String getPackageReferenceName() { return packageReferenceName; } 19 | 20 | private final String version; 21 | public String getVersion() { return version; } 22 | 23 | public SelectedPackage(String stepName, String packageId, String packageReferenceName, String version) { 24 | this.stepName = stepName; 25 | this.packageId = packageId; 26 | this.packageReferenceName = packageReferenceName; 27 | this.version = version; 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return "SelectedPackage [stepName=" + stepName + ", packageId=" + packageId + ", packageReferenceName=" + packageReferenceName + ", version=" + version + "]"; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/data/Task.java: -------------------------------------------------------------------------------- 1 | 2 | package com.octopusdeploy.api.data; 3 | 4 | /** 5 | * Task. 6 | * Tasks are what octopus uses when it is doing something. 7 | */ 8 | public class Task { 9 | private final String id; 10 | public String getId() { 11 | return id; 12 | } 13 | 14 | private final String name; 15 | public String getName() { 16 | return name; 17 | } 18 | 19 | private final String description; 20 | public String getDescription() { 21 | return description; 22 | } 23 | 24 | private final String state; 25 | public String getState() { 26 | return state; 27 | } 28 | 29 | private final boolean isCompleted; 30 | public boolean getIsCompleted() { 31 | return isCompleted; 32 | } 33 | 34 | public Task(String id, String name, String description, String state, boolean isCompleted) { 35 | this.id = id; 36 | this.name = name; 37 | this.description = description; 38 | this.state = state; 39 | this.isCompleted = isCompleted; 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | return "Task [id=" + id + ", name=" + name + ", description=" + description + ", state=" + state + ", isCompleted=" + isCompleted + "]"; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/data/Tenant.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api.data; 2 | 3 | /** 4 | * Represents a Tenant. 5 | */ 6 | public class Tenant { 7 | private final String name; 8 | public String getName() { 9 | return name; 10 | } 11 | 12 | private final String id; 13 | public String getId() { 14 | return id; 15 | } 16 | 17 | public Tenant(String id, String name) 18 | { 19 | this.id = id; 20 | this.name = name; 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | return "Tenant [name=" + name + ", id=" + id + "]"; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/octopusdeploy/api/data/Variable.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api.data; 2 | 3 | /** 4 | * Represents a variable that will be passed to a deployment. 5 | */ 6 | public class Variable { 7 | private final String name; 8 | public String getName() { 9 | return name; 10 | } 11 | 12 | private final String value; 13 | public String getValue() { 14 | return value; 15 | } 16 | 17 | private final String id; 18 | public String getId() { 19 | return id; 20 | } 21 | 22 | private final String description; 23 | public String getDescription() { 24 | return description; 25 | } 26 | 27 | public Variable(String id, String name, String value, String description) 28 | { 29 | this.id = id; 30 | this.name = name; 31 | this.value = value; 32 | this.description = description; 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return "Variable [name=" + name + ", value=" + value + ", id=" + id + ", description=" + description + "]"; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/octopusdeploy/AbstractOctopusDeployRecorder.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.octopusdeploy; 2 | 3 | import com.octopusdeploy.api.OctopusApi; 4 | import hudson.tasks.BuildStepMonitor; 5 | import hudson.tasks.Recorder; 6 | import jenkins.model.Jenkins; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | /** 12 | * The AbstractOctopusDeployRecorder tries to take care of most of the Octopus 13 | * Deploy server access. 14 | * @author wbenayed 15 | */ 16 | public abstract class AbstractOctopusDeployRecorder extends Recorder { 17 | 18 | /** 19 | * Cache for OctopusDeployServer instance used in deployment 20 | * transient keyword prevents leaking API key to Job configuration 21 | */ 22 | protected transient OctopusDeployServer octopusDeployServer; 23 | 24 | public OctopusDeployServer getOctopusDeployServer() { 25 | ///TODO use better approach to achieve Laziness 26 | if (octopusDeployServer == null) { 27 | octopusDeployServer = getOctopusDeployServer(getServerId()); 28 | } 29 | return octopusDeployServer; 30 | } 31 | 32 | /** 33 | * The serverId to use for this deployment 34 | */ 35 | protected String serverId; 36 | public String getServerId() { 37 | return serverId; 38 | } 39 | 40 | /** 41 | * The project name as defined in Octopus. 42 | */ 43 | protected String project; 44 | public String getProject() { 45 | return project; 46 | } 47 | 48 | /** 49 | * The environment to deploy to, if we are deploying. 50 | */ 51 | protected String environment; 52 | public String getEnvironment() { 53 | return environment; 54 | } 55 | 56 | /** 57 | * The Tenant to use for a deploy to in Octopus. 58 | */ 59 | protected String tenant; 60 | public String getTenant() { 61 | return tenant; 62 | } 63 | 64 | /** 65 | * Whether or not perform will return control immediately, or wait until the Deployment 66 | * task is completed. 67 | */ 68 | protected boolean waitForDeployment; 69 | public boolean getWaitForDeployment() { 70 | return waitForDeployment; 71 | } 72 | 73 | 74 | /** 75 | * Get the default OctopusDeployServer from OctopusDeployPlugin configuration 76 | * @return the default server 77 | * */ 78 | protected static OctopusDeployServer getDefaultOctopusDeployServer() { 79 | Jenkins jenkinsInstance = Jenkins.getInstance(); 80 | if (jenkinsInstance == null) { 81 | throw new IllegalStateException("Jenkins instance is null"); 82 | } 83 | OctopusDeployPlugin.DescriptorImpl descriptor = (OctopusDeployPlugin.DescriptorImpl) jenkinsInstance.getDescriptor(OctopusDeployPlugin.class); 84 | return descriptor.getDefaultOctopusDeployServer(); 85 | } 86 | 87 | /** 88 | * Get the list of OctopusDeployServer from OctopusDeployPlugin configuration 89 | * @return all configured servers 90 | * */ 91 | public static List getOctopusDeployServers() { 92 | Jenkins jenkinsInstance = Jenkins.getInstance(); 93 | if (jenkinsInstance == null) { 94 | throw new IllegalStateException("Jenkins instance is null"); 95 | } 96 | OctopusDeployPlugin.DescriptorImpl descriptor = (OctopusDeployPlugin.DescriptorImpl) jenkinsInstance.getDescriptor(OctopusDeployPlugin.class); 97 | return descriptor.getOctopusDeployServers(); 98 | } 99 | 100 | 101 | public static List getOctopusDeployServersIds() { 102 | 103 | List ids = new ArrayList<>(); 104 | for (OctopusDeployServer s:getOctopusDeployServers()) { 105 | ids.add(s.getId()); 106 | } 107 | return ids; 108 | } 109 | 110 | /** 111 | * Get the instance of OctopusDeployServer by serverId 112 | * @return the server by id 113 | * */ 114 | public static OctopusDeployServer getOctopusDeployServer(String serverId) { 115 | if (serverId == null || serverId.isEmpty()){ 116 | return getDefaultOctopusDeployServer(); 117 | } 118 | for(OctopusDeployServer server : getOctopusDeployServers()) { 119 | if(server.getId().equals(serverId)) { 120 | return server; 121 | } 122 | } 123 | return null; 124 | } 125 | 126 | 127 | /** 128 | * Get OctopusApi instance for this deployment 129 | * @return the api for a given server 130 | */ 131 | public OctopusApi getApi() { 132 | return getOctopusDeployServer().getApi(); 133 | } 134 | 135 | 136 | @Override 137 | public BuildStepMonitor getRequiredMonitorService() { 138 | return BuildStepMonitor.NONE; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/octopusdeploy/BuildInfoSummary.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.octopusdeploy; 2 | 3 | import hudson.model.BuildBadgeAction; 4 | 5 | /** 6 | * Plugin portion to show information on the Build Summary page, and on the line entries for build history. 7 | */ 8 | public class BuildInfoSummary implements BuildBadgeAction { 9 | 10 | private final OctopusDeployEventType buildResultType; 11 | private final String urlName; 12 | 13 | public BuildInfoSummary(OctopusDeployEventType buildResultType, String urlName) { 14 | this.buildResultType = buildResultType; 15 | this.urlName = urlName; 16 | } 17 | 18 | /** 19 | * The default file for the image used by this summary entry. 20 | * @return relative path to a file. 21 | */ 22 | @Override 23 | public String getIconFileName() { 24 | return "/plugin/octopusdeploy/images/octopus-o.png"; 25 | } 26 | 27 | /** 28 | * Get an icon that is differentiated depending on which kind of 29 | * action this is representing. 30 | * @return icon file path. 31 | */ 32 | public String getLabelledIconFileName() { 33 | String filename; 34 | switch (buildResultType) { 35 | case Deployment: 36 | filename = "octopus-d.png"; 37 | break; 38 | case Release: 39 | filename = "octopus-r.png"; 40 | break; 41 | default: 42 | filename = "octopus-o.png"; 43 | break; 44 | } 45 | return "/plugin/octopusdeploy/images/" + filename; 46 | } 47 | 48 | /** 49 | * Display name for this summary entry. 50 | * @return OctopusDeploy - [the type of action this represents] 51 | */ 52 | @Override 53 | public String getDisplayName() { 54 | return "OctopusDeploy - " + buildResultType; 55 | } 56 | 57 | /** 58 | * The URL to use in this summary entry. 59 | * @return URL to link to. 60 | */ 61 | @Override 62 | public String getUrlName() { 63 | return urlName; 64 | } 65 | 66 | /** 67 | * The types of OctopusDeploy even that this class can represent. 68 | */ 69 | public enum OctopusDeployEventType { 70 | Deployment, Release 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/octopusdeploy/EnvironmentVariableValueInjector.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.octopusdeploy; 2 | 3 | import hudson.EnvVars; 4 | import hudson.util.VariableResolver; 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | /** 9 | * Injects environment variable values into a string. 10 | */ 11 | public class EnvironmentVariableValueInjector { 12 | private final Pattern pattern; 13 | private final VariableResolver resolver; 14 | private final EnvVars environment; 15 | 16 | public EnvironmentVariableValueInjector(VariableResolver resolver, EnvVars environment) { 17 | pattern = Pattern.compile("\\$\\{(?[^\\}]+)\\}"); 18 | this.resolver = resolver; 19 | this.environment = environment; 20 | } 21 | 22 | /** 23 | * Takes a string possibly containing tokens that represent Environment Variables and replaces them with the variables' values. 24 | * If the variable is not defined, the token is not replaced. 25 | * First looks in environment variables, then looks at the build variable resolver for values. 26 | * @param candidate the candidate string possibly containing env tokens. 27 | * @return a new string with all possible tokens replaced with values. 28 | */ 29 | public String injectEnvironmentVariableValues(String candidate) { 30 | if (candidate == null || candidate.isEmpty() || !candidate.contains("${")) { // Early exit 31 | return candidate; 32 | } 33 | String resolved = candidate; 34 | int locatedMatch = 0; 35 | Matcher matcher = pattern.matcher(resolved); 36 | while (matcher.find(locatedMatch)) { 37 | String variableName = matcher.group("variable"); 38 | locatedMatch = matcher.end(); 39 | Object resolvedVariable = environment.get(variableName); 40 | if (resolvedVariable == null) { 41 | resolvedVariable = resolver.resolve(variableName); 42 | } 43 | if (resolvedVariable != null) { 44 | resolved = resolved.replace(String.format("${%s}", variableName), resolvedVariable.toString()); 45 | } 46 | } 47 | 48 | return resolved; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/octopusdeploy/JSONSanitizer.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.octopusdeploy; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * Simple JSON sanitizer to allow special characters in JSON input. It also replaces 8 | * control characters (newline, tab) and replaces them with html-friendly versions 9 | * @author jlabroad 10 | */ 11 | public class JSONSanitizer { 12 | private static JSONSanitizer instance = null; 13 | 14 | /** Characters that need to be replaced with something else */ 15 | private HashMap replacementChars = null; 16 | 17 | private JSONSanitizer() { 18 | replacementChars = new HashMap(); 19 | replacementChars.put("\"", "\\\\\""); 20 | replacementChars.put("\n", "
"); //Replace new line with html line break 21 | replacementChars.put("\t", "    "); //Replace tab with 4 spaces 22 | } 23 | 24 | public static JSONSanitizer getInstance() { 25 | if (instance == null ) { 26 | instance = new JSONSanitizer(); 27 | } 28 | return instance; 29 | } 30 | 31 | /** 32 | * Sanitizes the input string so that it can be represented in JSON 33 | * @param dirtyString The un-sanitized string 34 | * @return The sanitized string that can be directly added to a JSON command 35 | */ 36 | public String sanitize(String dirtyString) { 37 | String sanitized = dirtyString; 38 | 39 | // Handle backslashes first. All backslashes that remain after this are for escaping purposes 40 | sanitized = sanitized.replaceAll("\\\\", "\\\\u005C"); 41 | 42 | // Make all the replacements 43 | for (Map.Entry charPair : replacementChars.entrySet()) { 44 | sanitized = sanitized.replaceAll(charPair.getKey(), charPair.getValue()); 45 | } 46 | 47 | return sanitized; 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/octopusdeploy/Log.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.octopusdeploy; 2 | 3 | import hudson.model.BuildListener; 4 | import java.io.PrintStream; 5 | 6 | /** 7 | * Logs messages to the Jenkins build console. 8 | * @author cwetherby 9 | */ 10 | public class Log { 11 | private final BuildListener listener; 12 | private final PrintStream logger; 13 | 14 | /** 15 | * Generate a log that adds lines to the given BuildListener's console output. 16 | * @param listener The BuildListener responsible for adding lines to the job's console. 17 | */ 18 | public Log(BuildListener listener) { 19 | this.listener = listener; 20 | this.logger = listener.getLogger(); 21 | } 22 | 23 | /** 24 | * Print an info message. 25 | * @param msg The info message. 26 | */ 27 | public void info(String msg) { 28 | logger.append("INFO: " + msg + "\n"); 29 | } 30 | 31 | /** 32 | * Print an error message. 33 | * @param msg The error message. 34 | */ 35 | public void error(String msg) { 36 | listener.error(msg); 37 | } 38 | 39 | /** 40 | * Print a fatal error message. 41 | * @param msg The fatal error message. 42 | */ 43 | public void fatal(String msg) { 44 | listener.fatalError(msg); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/octopusdeploy/OctopusDeployDeploymentRecorder.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.octopusdeploy; 2 | 3 | import com.octopusdeploy.api.data.Task; 4 | import com.octopusdeploy.api.data.Release; 5 | import com.octopusdeploy.api.*; 6 | import java.io.*; 7 | import java.util.*; 8 | import hudson.*; 9 | import hudson.model.*; 10 | import hudson.tasks.*; 11 | import hudson.util.*; 12 | import java.util.logging.Level; 13 | import java.util.logging.Logger; 14 | import net.sf.json.*; 15 | import org.kohsuke.stapler.*; 16 | 17 | /** 18 | * Executes deployments of releases. 19 | */ 20 | public class OctopusDeployDeploymentRecorder extends AbstractOctopusDeployRecorder implements Serializable { 21 | 22 | /** 23 | * The release version number in Octopus. 24 | */ 25 | private final String releaseVersion; 26 | public String getReleaseVersion() { 27 | return releaseVersion; 28 | } 29 | 30 | /** 31 | * The variables to use for a deploy to in Octopus. 32 | */ 33 | private final String variables; 34 | public String getVariables() { 35 | return variables; 36 | } 37 | 38 | @DataBoundConstructor 39 | public OctopusDeployDeploymentRecorder(String serverId, String project, String releaseVersion, String environment, String tenant, String variables, boolean waitForDeployment) { 40 | this.serverId = serverId.trim(); 41 | this.project = project.trim(); 42 | this.releaseVersion = releaseVersion.trim(); 43 | this.environment = environment.trim(); 44 | this.tenant = tenant == null ? null : tenant.trim(); // Otherwise this can throw on plugin version upgrade 45 | this.variables = variables.trim(); 46 | this.waitForDeployment = waitForDeployment; 47 | } 48 | 49 | @Override 50 | public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) { 51 | // This method deserves a refactor and cleanup. 52 | boolean success = true; 53 | Log log = new Log(listener); 54 | if (Result.FAILURE.equals(build.getResult())) { 55 | log.info("Not deploying due to job being in FAILED state."); 56 | return success; 57 | } 58 | 59 | logStartHeader(log); 60 | 61 | VariableResolver resolver = build.getBuildVariableResolver(); 62 | EnvVars envVars; 63 | try { 64 | envVars = build.getEnvironment(listener); 65 | } catch (Exception ex) { 66 | log.fatal(String.format("Failed to retrieve environment variables for this build - '%s'", ex.getMessage())); 67 | return false; 68 | } 69 | EnvironmentVariableValueInjector envInjector = new EnvironmentVariableValueInjector(resolver, envVars); 70 | // NOTE: hiding the member variables of the same name with their env-injected equivalents 71 | String project = envInjector.injectEnvironmentVariableValues(this.project); 72 | String releaseVersion = envInjector.injectEnvironmentVariableValues(this.releaseVersion); 73 | String environment = envInjector.injectEnvironmentVariableValues(this.environment); 74 | String tenant = envInjector.injectEnvironmentVariableValues(this.tenant); 75 | String variables = envInjector.injectEnvironmentVariableValues(this.variables); 76 | 77 | com.octopusdeploy.api.data.Project p = null; 78 | try { 79 | p = getApi().getProjectsApi().getProjectByName(project); 80 | } catch (Exception ex) { 81 | log.fatal(String.format("Retrieving project name '%s' failed with message '%s'", 82 | project, ex.getMessage())); 83 | success = false; 84 | } 85 | com.octopusdeploy.api.data.Environment env = null; 86 | try { 87 | env = getApi().getEnvironmentsApi().getEnvironmentByName(environment); 88 | } catch (Exception ex) { 89 | log.fatal(String.format("Retrieving environment name '%s' failed with message '%s'", 90 | environment, ex.getMessage())); 91 | success = false; 92 | } 93 | 94 | if (p == null) { 95 | log.fatal("Project was not found."); 96 | success = false; 97 | } 98 | if (env == null) { 99 | log.fatal("Environment was not found."); 100 | success = false; 101 | } 102 | if (!success) // Early exit 103 | { 104 | return success; 105 | } 106 | 107 | String tenantId = null; 108 | if (tenant != null && !tenant.isEmpty()) { 109 | com.octopusdeploy.api.data.Tenant ten = null; 110 | try { 111 | ten = getApi().getTenantsApi().getTenantByName(tenant); 112 | if (ten != null) { 113 | tenantId = ten.getId(); 114 | } else { 115 | log.fatal(String.format("Retrieving tenant name '%s' failed with message 'not found'", tenant)); 116 | return false; 117 | } 118 | } catch (Exception ex) { 119 | log.fatal(String.format("Retrieving tenant name '%s' failed with message '%s'", 120 | tenant, ex.getMessage())); 121 | return false; 122 | } 123 | } 124 | 125 | Set releases = null; 126 | try { 127 | releases = getApi().getReleasesApi().getReleasesForProject(p.getId()); 128 | } catch (Exception ex) { 129 | log.fatal(String.format("Retrieving releases for project '%s' failed with message '%s'", 130 | project, ex.getMessage())); 131 | success = false; 132 | } 133 | if (releases == null) { 134 | log.fatal("Releases was not found."); 135 | return false; 136 | } 137 | Release releaseToDeploy = null; 138 | for(Release r : releases) { 139 | if (releaseVersion.equals(r.getVersion())) 140 | { 141 | releaseToDeploy = r; 142 | break; 143 | } 144 | } 145 | if (releaseToDeploy == null) // early exit 146 | { 147 | log.fatal(String.format("Unable to find release version %s for project %s", releaseVersion, project)); 148 | return false; 149 | } 150 | Properties properties = new Properties(); 151 | try { 152 | properties.load(new StringReader(variables)); 153 | } catch (Exception ex) { 154 | log.fatal(String.format("Unable to load entry variables failed with message '%s'", 155 | ex.getMessage())); 156 | success = false; 157 | } 158 | 159 | // TODO: Can we tell if we need to call? For now I will always try and get variable and use if I find them 160 | Set variablesForDeploy = null; 161 | 162 | try { 163 | String releaseId = releaseToDeploy.getId(); 164 | String environmentId = env.getId(); 165 | variablesForDeploy = getApi().getVariablesApi().getVariablesByReleaseAndEnvironment(releaseId, environmentId, properties); 166 | } catch (Exception ex) { 167 | log.fatal(String.format("Retrieving variables for release '%s' to environment '%s' failed with message '%s'", 168 | releaseToDeploy.getId(), env.getName(), ex.getMessage())); 169 | success = false; 170 | } 171 | try { 172 | String results = getApi().getDeploymentsApi().executeDeployment(releaseToDeploy.getId(), env.getId(), tenantId, variablesForDeploy); 173 | if (isTaskJson(results)) { 174 | JSON resultJson = JSONSerializer.toJSON(results); 175 | String urlSuffix = ((JSONObject)resultJson).getJSONObject("Links").getString("Web"); 176 | String url = getOctopusDeployServer().getUrl(); 177 | if (url.endsWith("/")) { 178 | url = url.substring(0, url.length() - 1); 179 | } 180 | log.info("Deployment executed: \n\t" + url + urlSuffix); 181 | build.addAction(new BuildInfoSummary(BuildInfoSummary.OctopusDeployEventType.Deployment, url + urlSuffix)); 182 | if (waitForDeployment) { 183 | log.info("Waiting for deployment to complete."); 184 | String resultState = waitForDeploymentCompletion(resultJson, getApi(), log); 185 | if (resultState == null) { 186 | log.info("Marking build failed due to failure in waiting for deployment to complete."); 187 | success = false; 188 | } 189 | 190 | if ("Failed".equals(resultState)) { 191 | log.info("Marking build failed due to deployment task status."); 192 | success = false; 193 | } 194 | } 195 | } 196 | } catch (IOException ex) { 197 | log.fatal("Failed to deploy: " + ex.getMessage()); 198 | success = false; 199 | } 200 | 201 | return success; 202 | } 203 | 204 | private DescriptorImpl getDescriptorImpl() { 205 | return ((DescriptorImpl)getDescriptor()); 206 | } 207 | 208 | /** 209 | * Write the startup header for the logs to show what our inputs are. 210 | * @param log The logger 211 | */ 212 | private void logStartHeader(Log log) { 213 | log.info("Started Octopus Deploy"); 214 | log.info("======================"); 215 | log.info("Project: " + project); 216 | log.info("Version: " + releaseVersion); 217 | log.info("Environment: " + environment); 218 | if (tenant != null && !tenant.isEmpty()) { 219 | log.info("Tenant: " + tenant); 220 | } 221 | log.info("======================"); 222 | } 223 | 224 | /** 225 | * Attempts to parse the string as JSON. 226 | * returns true on success 227 | * @param possiblyJson A string that may be JSON 228 | * @return true or false. True if string is valid JSON. 229 | */ 230 | private boolean isTaskJson(String possiblyJson) { 231 | try { 232 | JSONSerializer.toJSON(possiblyJson); 233 | return true; 234 | } catch (JSONException ex) { 235 | return false; 236 | } 237 | } 238 | 239 | /** 240 | * Returns control when task is complete. 241 | * @param json json input 242 | * @param api octopus api 243 | * @param logger logger 244 | * @return the task state for the deployment 245 | */ 246 | private String waitForDeploymentCompletion(JSON json, OctopusApi api, Log logger) { 247 | final long WAIT_TIME = 5000; 248 | final double WAIT_RANDOM_SCALER = 100.0; 249 | JSONObject jsonObj = (JSONObject)json; 250 | String id = jsonObj.getString("TaskId"); 251 | Task task = null; 252 | String lastState = "Unknown"; 253 | try { 254 | task = api.getTasksApi().getTask(id); 255 | } catch (IOException ex) { 256 | logger.error("Error getting task: " + ex.getMessage()); 257 | return null; 258 | } 259 | 260 | logger.info("Task info:"); 261 | logger.info("\tId: " + task.getId()); 262 | logger.info("\tName: " + task.getName()); 263 | logger.info("\tDesc: " + task.getDescription()); 264 | logger.info("\tState: " + task.getState()); 265 | logger.info("\n\nStarting wait..."); 266 | boolean completed = task.getIsCompleted(); 267 | while (!completed) 268 | { 269 | try { 270 | task = api.getTasksApi().getTask(id); 271 | } catch (IOException ex) { 272 | logger.error("Error getting task: " + ex.getMessage()); 273 | return null; 274 | } 275 | 276 | completed = task.getIsCompleted(); 277 | lastState = task.getState(); 278 | logger.info("Task state: " + lastState); 279 | if (completed) { 280 | break; 281 | } 282 | try { 283 | Thread.sleep(WAIT_TIME + (long)(Math.random() * WAIT_RANDOM_SCALER)); 284 | } catch (InterruptedException ex) { 285 | logger.info("Wait interrupted!"); 286 | logger.info(ex.getMessage()); 287 | completed = true; // bail out of wait loop 288 | } 289 | } 290 | logger.info("Wait complete!"); 291 | return lastState; 292 | } 293 | 294 | /** 295 | * Descriptor for {@link OctopusDeployDeploymentRecorder}. Used as a singleton. 296 | * The class is marked as public so that it can be accessed from views. 297 | */ 298 | @Extension 299 | public static final class DescriptorImpl extends BuildStepDescriptor { 300 | private static final String PROJECT_RELEASE_VALIDATION_MESSAGE = "Project must be set to validate release."; 301 | private static final String SERVER_ID_VALIDATION_MESSAGE = "Could not validate without a valid Server ID."; 302 | 303 | public DescriptorImpl() { 304 | load(); 305 | } 306 | 307 | @Override 308 | public boolean isApplicable(Class aClass) { 309 | return true; 310 | } 311 | 312 | @Override 313 | public String getDisplayName() { 314 | return "OctopusDeploy Deployment"; 315 | } 316 | 317 | @Override 318 | public boolean configure(StaplerRequest req, JSONObject formData) throws Descriptor.FormException { 319 | save(); 320 | return true; 321 | } 322 | 323 | private OctopusApi getApiByServerId(String serverId){ 324 | return AbstractOctopusDeployRecorder.getOctopusDeployServer(serverId).getApi(); 325 | } 326 | 327 | public String getDefaultOctopusDeployServerId() { 328 | 329 | OctopusDeployServer server = AbstractOctopusDeployRecorder.getDefaultOctopusDeployServer(); 330 | if(server != null){ 331 | return server.getId(); 332 | } 333 | return null; 334 | } 335 | 336 | /** 337 | * Check that the serverId field is not empty. 338 | * @param serverId The id of OctopusDeployServer in the configuration. 339 | * @return Ok if not empty, error otherwise. 340 | */ 341 | public FormValidation doCheckServerId(@QueryParameter String serverId) { 342 | 343 | serverId = serverId.trim(); 344 | return OctopusValidator.validateServerId(serverId); 345 | } 346 | 347 | /** 348 | * Check that the project field is not empty and is a valid project. 349 | * @param project The name of the project. 350 | * @param serverId The id of OctopusDeployServer in the configuration. 351 | * @return Ok if not empty, error otherwise. 352 | */ 353 | public FormValidation doCheckProject(@QueryParameter String project, @QueryParameter String serverId) { 354 | project = project.trim(); 355 | 356 | if (doCheckServerId(serverId).kind != FormValidation.Kind.OK) { 357 | return FormValidation.warning(SERVER_ID_VALIDATION_MESSAGE); 358 | } 359 | 360 | OctopusApi api = getApiByServerId(serverId); 361 | OctopusValidator validator = new OctopusValidator(api); 362 | return validator.validateProject(project); 363 | } 364 | 365 | /** 366 | * Check that the releaseVersion field is not empty. 367 | * @param releaseVersion The release version of the package. 368 | * @param project The project name 369 | * @param serverId The id of OctopusDeployServer in the configuration. 370 | * @return Ok if not empty, error otherwise. 371 | */ 372 | public FormValidation doCheckReleaseVersion(@QueryParameter String releaseVersion, @QueryParameter String project, @QueryParameter String serverId) { 373 | releaseVersion = releaseVersion.trim(); 374 | 375 | if (doCheckServerId(serverId).kind != FormValidation.Kind.OK) { 376 | return FormValidation.warning(SERVER_ID_VALIDATION_MESSAGE); 377 | } 378 | 379 | OctopusApi api = getApiByServerId(serverId); 380 | if (project == null || project.isEmpty()) { 381 | return FormValidation.warning(PROJECT_RELEASE_VALIDATION_MESSAGE); 382 | } 383 | com.octopusdeploy.api.data.Project p; 384 | try { 385 | p = api.getProjectsApi().getProjectByName(project); 386 | if (p == null) { 387 | return FormValidation.warning(PROJECT_RELEASE_VALIDATION_MESSAGE); 388 | } 389 | } catch (Exception ex) { 390 | return FormValidation.warning(PROJECT_RELEASE_VALIDATION_MESSAGE); 391 | } 392 | 393 | OctopusValidator validator = new OctopusValidator(api); 394 | return validator.validateRelease(releaseVersion, p.getId(), OctopusValidator.ReleaseExistenceRequirement.MustExist); 395 | } 396 | 397 | 398 | /** 399 | * Check that the environment field is not empty. 400 | * @param environment The name of the project. 401 | * @param serverId The id of OctopusDeployServer in the configuration. 402 | * @return Ok if not empty, error otherwise. 403 | */ 404 | public FormValidation doCheckEnvironment(@QueryParameter String environment, @QueryParameter String serverId) { 405 | environment = environment.trim(); 406 | 407 | if (doCheckServerId(serverId).kind != FormValidation.Kind.OK) { 408 | return FormValidation.warning(SERVER_ID_VALIDATION_MESSAGE); 409 | } 410 | 411 | OctopusApi api = getApiByServerId(serverId); 412 | OctopusValidator validator = new OctopusValidator(api); 413 | return validator.validateEnvironment(environment); 414 | } 415 | 416 | /** 417 | * Data binding that returns all configured Octopus server ids to be used in the serverId drop-down list. 418 | * @return ComboBoxModel 419 | */ 420 | public ComboBoxModel doFillServerIdItems() { 421 | 422 | return new ComboBoxModel(getOctopusDeployServersIds()); 423 | } 424 | 425 | /** 426 | * Data binding that returns all possible environment names to be used in the environment autocomplete. 427 | * @param serverId The id of OctopusDeployServer in the configuration. 428 | * @return ComboBoxModel 429 | */ 430 | public ComboBoxModel doFillEnvironmentItems(@QueryParameter String serverId) { 431 | ComboBoxModel names = new ComboBoxModel(); 432 | 433 | if (doCheckServerId(serverId).kind != FormValidation.Kind.OK) { 434 | return names; 435 | } 436 | 437 | OctopusApi api = getApiByServerId(serverId); 438 | try { 439 | Set environments = api.getEnvironmentsApi().getAllEnvironments(); 440 | for (com.octopusdeploy.api.data.Environment env : environments) { 441 | names.add(env.getName()); 442 | } 443 | } catch (Exception ex) { 444 | Logger.getLogger(OctopusDeployDeploymentRecorder.class.getName()).log(Level.SEVERE, null, ex); 445 | } 446 | return names; 447 | } 448 | 449 | /** 450 | * Data binding that returns all possible project names to be used in the project autocomplete. 451 | * @param serverId The id of OctopusDeployServer in the configuration. 452 | * @return ComboBoxModel 453 | */ 454 | public ComboBoxModel doFillProjectItems(@QueryParameter String serverId) { 455 | ComboBoxModel names = new ComboBoxModel(); 456 | 457 | if (doCheckServerId(serverId).kind != FormValidation.Kind.OK) { 458 | return names; 459 | } 460 | 461 | OctopusApi api = getApiByServerId(serverId); 462 | try { 463 | Set projects = api.getProjectsApi().getAllProjects(); 464 | for (com.octopusdeploy.api.data.Project proj : projects) { 465 | names.add(proj.getName()); 466 | } 467 | } catch (Exception ex) { 468 | Logger.getLogger(OctopusDeployDeploymentRecorder.class.getName()).log(Level.SEVERE, null, ex); 469 | } 470 | return names; 471 | } 472 | 473 | /** 474 | * Data binding that returns all possible tenant names to be used in the tenant autocomplete. 475 | * @param serverId The id of OctopusDeployServer in the configuration. 476 | * @return ComboBoxModel 477 | */ 478 | public ComboBoxModel doFillTenantItems(@QueryParameter String serverId) { 479 | ComboBoxModel names = new ComboBoxModel(); 480 | 481 | if (doCheckServerId(serverId).kind != FormValidation.Kind.OK) { 482 | return names; 483 | } 484 | 485 | OctopusApi api = getApiByServerId(serverId); 486 | try { 487 | Set tenants = api.getTenantsApi().getAllTenants(); 488 | for (com.octopusdeploy.api.data.Tenant ten : tenants) { 489 | names.add(ten.getName()); 490 | } 491 | } catch (Exception ex) { 492 | Logger.getLogger(OctopusDeployDeploymentRecorder.class.getName()).log(Level.SEVERE, null, ex); 493 | } 494 | return names; 495 | } 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/octopusdeploy/OctopusDeployPlugin.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.octopusdeploy; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import hudson.Extension; 5 | import hudson.model.Descriptor; 6 | import hudson.util.FormValidation; 7 | import jenkins.model.Jenkins; 8 | import jenkins.model.GlobalConfiguration; 9 | import jenkins.model.GlobalPluginConfiguration; 10 | import net.sf.json.JSONObject; 11 | import org.kohsuke.stapler.QueryParameter; 12 | import org.kohsuke.stapler.StaplerRequest; 13 | import org.kohsuke.stapler.interceptor.RequirePOST; 14 | 15 | import java.io.IOException; 16 | import java.net.HttpURLConnection; 17 | import java.net.MalformedURLException; 18 | import java.net.URL; 19 | import java.net.URLConnection; 20 | import java.util.ArrayList; 21 | import java.util.Collections; 22 | import java.util.List; 23 | import java.util.logging.Level; 24 | import java.util.logging.Logger; 25 | 26 | /** 27 | * This plugin is only responsible for containing global configuration information 28 | * to be used by the ReleaseRecorder and DeploymentRecorder. 29 | * @author badriance 30 | */ 31 | public class OctopusDeployPlugin extends GlobalPluginConfiguration { 32 | 33 | @Extension 34 | public static final class DescriptorImpl extends Descriptor { 35 | 36 | private transient String apiKey; 37 | 38 | private transient String octopusHost; 39 | 40 | /** 41 | * Get the default OctopusDeployServer instance 42 | * @return the default server 43 | */ 44 | public OctopusDeployServer getDefaultOctopusDeployServer() { 45 | 46 | for (OctopusDeployServer s : getOctopusDeployServers()) { 47 | if (s.isDefault()) { 48 | return s; 49 | } 50 | } 51 | if(!getOctopusDeployServers().isEmpty()) { 52 | return getOctopusDeployServers().get(0); 53 | } 54 | return null; 55 | } 56 | 57 | private List octopusDeployServers; 58 | public List getOctopusDeployServers() { 59 | if (octopusDeployServers != null) { 60 | return octopusDeployServers; 61 | } 62 | return Collections.emptyList(); 63 | } 64 | 65 | private void setOctopusDeployServers(List servers) { 66 | octopusDeployServers = servers; 67 | } 68 | 69 | public DescriptorImpl() { 70 | load(); 71 | loadLegacyOctopusDeployServerConfig(); 72 | } 73 | 74 | /** 75 | * Load legacy OctopusPlugin configuration format 76 | */ 77 | @SuppressFBWarnings(value = "UWF_UNWRITTEN_FIELD", justification = "This is for backwards compatiblity on Jenkins plugin upgrade" ) 78 | private void loadLegacyOctopusDeployServerConfig() { 79 | if (doesLegacyOctopusDeployServerExist()){ 80 | OctopusDeployServer server = new OctopusDeployServer("default", octopusHost, apiKey, true); 81 | if(octopusDeployServers == null) 82 | { 83 | octopusDeployServers = new ArrayList<>(); 84 | } 85 | octopusDeployServers.add(0, server); 86 | } 87 | } 88 | 89 | private boolean doesLegacyOctopusDeployServerExist() { 90 | return octopusHost != null && apiKey !=null; 91 | } 92 | 93 | @Override 94 | public String getDisplayName() { 95 | return "OctopusDeploy Plugin"; 96 | } 97 | 98 | /** 99 | * Validate that serverId is: 100 | * - Not empty 101 | * - Unique 102 | * @param serverId the uniqueId for an octopus deploy instance 103 | * @return Form validation to present on the Jenkins UI 104 | */ 105 | public FormValidation doCheckServerId(@QueryParameter String serverId,@QueryParameter String url,@QueryParameter String apiKey) { 106 | serverId = serverId.trim(); 107 | if (serverId.isEmpty()) { 108 | return FormValidation.warning("Please set a ServerID"); 109 | } 110 | for (OctopusDeployServer s:getOctopusDeployServers()){ 111 | if (serverId.equals(s.getId()) && !url.equals(s.getUrl()) && !apiKey.equals(s.getApiKey())){ 112 | return FormValidation.error("The Server ID you entered already exists."); 113 | } 114 | } 115 | return FormValidation.ok(); 116 | } 117 | 118 | /** 119 | * Validate that the host is: 120 | * - Not empty 121 | * - A well formed URL 122 | * - A real location that we can connect to 123 | * @param url the host URL for the octopus deploy instance 124 | * @return Form validation to present on the Jenkins UI 125 | */ 126 | @RequirePOST 127 | public FormValidation doCheckUrl(@QueryParameter String url) { 128 | if (url.isEmpty()) { 129 | return FormValidation.warning("Please enter a url to your OctopusDeploy Host"); 130 | } 131 | Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); 132 | try { 133 | 134 | URLConnection connection = new URL(url).openConnection(); 135 | if (connection instanceof HttpURLConnection) { 136 | HttpURLConnection httpConnection = (HttpURLConnection) connection; 137 | httpConnection.setRequestMethod("HEAD"); 138 | int code = httpConnection.getResponseCode(); 139 | httpConnection.disconnect(); 140 | if (code >= 400) { 141 | return FormValidation.error("Could not connect. HTTP Response %s", code); 142 | } 143 | } 144 | } 145 | catch (MalformedURLException ex) { 146 | final String INVALID_URL = "Supplied Octopus Host URL is invalid"; 147 | Logger.getLogger(OctopusDeployPlugin.class.getName()).log(Level.WARNING, INVALID_URL, ex); 148 | return FormValidation.error(INVALID_URL); 149 | } 150 | catch (IOException ex) { 151 | final String UNABLE_TO_CONNECT = "Unable to connect to Octopus Host URL"; 152 | Logger.getLogger(OctopusDeployPlugin.class.getName()).log(Level.WARNING, UNABLE_TO_CONNECT, ex); 153 | return FormValidation.error("%s - %s", UNABLE_TO_CONNECT, ex.getMessage()); 154 | } 155 | 156 | return FormValidation.ok(); 157 | } 158 | 159 | /** 160 | * Validate that the apiKey is: 161 | * - Not empty 162 | * - has a valid OctopusDeploy API Key format: API-XXXXXXXXX 163 | * @param apiKey Octopus API Key used for deployment 164 | * @return Form validation to present on the Jenkins UI 165 | */ 166 | public FormValidation doCheckApiKey(@QueryParameter String apiKey) { 167 | apiKey = apiKey.trim(); 168 | if (apiKey.isEmpty()) { 169 | return FormValidation.warning("Please set a API Key generated from OctopusDeploy Server."); 170 | } 171 | if (!apiKey.matches("API\\-\\w{25,27}")) { 172 | return FormValidation.error("Supplied Octopus API Key format is invalid. It should look like API-XXXXXXXXXXXXXXXXXXXXXXXXXXX"); 173 | } 174 | return FormValidation.ok(); 175 | } 176 | 177 | @Override 178 | public boolean configure(StaplerRequest req, JSONObject formData) throws Descriptor.FormException { 179 | List servers = null; 180 | JSONObject json = formData.getJSONObject("octopusConfig"); 181 | 182 | if (!json.isEmpty()) { 183 | servers = req.bindJSONToList(OctopusDeployServer.class, json.get("servers")); 184 | } 185 | setOctopusDeployServers(servers); 186 | 187 | save(); 188 | return super.configure(req, formData); 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/octopusdeploy/OctopusDeployReleaseRecorder.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.octopusdeploy; 2 | 3 | import com.octopusdeploy.api.data.SelectedPackage; 4 | import com.octopusdeploy.api.data.DeploymentProcessTemplate; 5 | import com.octopusdeploy.api.*; 6 | import java.io.*; 7 | import java.nio.charset.StandardCharsets; 8 | import java.nio.file.Files; 9 | import java.util.*; 10 | 11 | import hudson.*; 12 | import hudson.FilePath.FileCallable; 13 | import hudson.model.*; 14 | import hudson.remoting.VirtualChannel; 15 | import hudson.scm.*; 16 | import hudson.tasks.*; 17 | import hudson.util.*; 18 | import java.util.logging.Level; 19 | import java.util.logging.Logger; 20 | import net.sf.json.*; 21 | import org.apache.commons.lang.StringUtils; 22 | import org.jenkinsci.remoting.RoleChecker; 23 | import org.kohsuke.stapler.*; 24 | import org.kohsuke.stapler.export.*; 25 | 26 | /** 27 | * Creates a release and optionally deploys it. 28 | */ 29 | public class OctopusDeployReleaseRecorder extends AbstractOctopusDeployRecorder implements Serializable { 30 | /** 31 | * The release version as defined in Octopus. 32 | */ 33 | private final String releaseVersion; 34 | public String getReleaseVersion() { 35 | return releaseVersion; 36 | } 37 | 38 | /** 39 | * Are there release notes for this release? 40 | */ 41 | private final boolean releaseNotes; 42 | public boolean getReleaseNotes() { 43 | return releaseNotes; 44 | } 45 | 46 | /** 47 | * Where are the release notes located? 48 | */ 49 | private final String releaseNotesSource; 50 | public String getReleaseNotesSource() { 51 | return releaseNotesSource; 52 | } 53 | 54 | public boolean isReleaseNotesSourceFile() { 55 | return "file".equals(releaseNotesSource); 56 | } 57 | 58 | public boolean isReleaseNotesSourceScm() { 59 | return "scm".equals(releaseNotesSource); 60 | } 61 | 62 | private final String channel; 63 | public String getChannel() { 64 | return channel; 65 | } 66 | 67 | /** 68 | * Write a link back to the originating Jenkins build to the 69 | * Octopus release notes? 70 | */ 71 | private final boolean releaseNotesJenkinsLinkback; 72 | public boolean getJenkinsUrlLinkback() { 73 | return releaseNotesJenkinsLinkback; 74 | } 75 | 76 | /** 77 | * The file that the release notes are in. 78 | */ 79 | private final String releaseNotesFile; 80 | public String getReleaseNotesFile() { 81 | return releaseNotesFile; 82 | } 83 | 84 | /** 85 | * Should this release be deployed right after it is created? 86 | */ 87 | private final boolean deployThisRelease; 88 | @Exported 89 | public boolean getDeployThisRelease() { 90 | return deployThisRelease; 91 | } 92 | 93 | /** 94 | * All packages needed to create this new release. 95 | */ 96 | private final List packageConfigs; 97 | @Exported 98 | public List getPackageConfigs() { 99 | return packageConfigs; 100 | } 101 | 102 | /** 103 | * Default package version to use for required packages that are not 104 | * specified in the Package Configurations 105 | */ 106 | private final String defaultPackageVersion; 107 | @Exported 108 | public String getDefaultPackageVersion() { 109 | return defaultPackageVersion; 110 | } 111 | 112 | // Fields in config.jelly must match the parameter names in the "DataBoundConstructor" 113 | @DataBoundConstructor 114 | public OctopusDeployReleaseRecorder( 115 | String serverId, String project, String releaseVersion, 116 | boolean releaseNotes, String releaseNotesSource, String releaseNotesFile, 117 | boolean deployThisRelease, String environment, String tenant, String channel, boolean waitForDeployment, 118 | List packageConfigs, boolean jenkinsUrlLinkback, 119 | String defaultPackageVersion) { 120 | 121 | this.serverId = serverId.trim(); 122 | this.project = project.trim(); 123 | this.releaseVersion = releaseVersion.trim(); 124 | this.releaseNotes = releaseNotes; 125 | this.releaseNotesSource = releaseNotesSource; 126 | this.releaseNotesFile = releaseNotesFile.trim(); 127 | this.deployThisRelease = deployThisRelease; 128 | this.packageConfigs = packageConfigs; 129 | this.environment = environment.trim(); 130 | this.tenant = tenant == null ? null : tenant.trim(); 131 | this.channel = channel == null ? null : channel.trim(); 132 | this.waitForDeployment = waitForDeployment; 133 | this.releaseNotesJenkinsLinkback = jenkinsUrlLinkback; 134 | this.defaultPackageVersion = defaultPackageVersion; 135 | } 136 | 137 | @Override 138 | public BuildStepMonitor getRequiredMonitorService() { 139 | return BuildStepMonitor.NONE; 140 | } 141 | 142 | @Override 143 | public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) { 144 | boolean success = true; 145 | Log log = new Log(listener); 146 | if (Result.FAILURE.equals(build.getResult())) { 147 | log.info("Not creating a release due to job being in FAILED state."); 148 | return success; 149 | } 150 | logStartHeader(log); 151 | 152 | VariableResolver resolver = build.getBuildVariableResolver(); 153 | EnvVars envVars; 154 | try { 155 | envVars = build.getEnvironment(listener); 156 | } catch (Exception ex) { 157 | log.fatal(String.format("Failed to retrieve environment variables for this project '%s' - '%s'", 158 | project, ex.getMessage())); 159 | return false; 160 | } 161 | EnvironmentVariableValueInjector envInjector = new EnvironmentVariableValueInjector(resolver, envVars); 162 | 163 | // NOTE: hiding the member variables of the same name with their env-injected equivalents 164 | String project = envInjector.injectEnvironmentVariableValues(this.project); 165 | String releaseVersion = envInjector.injectEnvironmentVariableValues(this.releaseVersion); 166 | String releaseNotesFile = envInjector.injectEnvironmentVariableValues(this.releaseNotesFile); 167 | String environment = envInjector.injectEnvironmentVariableValues(this.environment); 168 | String tenant = envInjector.injectEnvironmentVariableValues(this.tenant); 169 | String channel = envInjector.injectEnvironmentVariableValues(this.channel); 170 | String defaultPackageVersion = envInjector.injectEnvironmentVariableValues(this.defaultPackageVersion); 171 | 172 | com.octopusdeploy.api.data.Project p = null; 173 | try { 174 | p = getApi().getProjectsApi().getProjectByName(project); 175 | } catch (Exception ex) { 176 | log.fatal(String.format("Retrieving project name '%s' failed with message '%s'", 177 | project, ex.getMessage())); 178 | success = false; 179 | } 180 | if (p == null) { 181 | log.fatal("Project was not found."); 182 | success = false; 183 | } 184 | 185 | com.octopusdeploy.api.data.Channel c = null; 186 | if (channel != null && !channel.isEmpty()) { 187 | try { 188 | c = getApi().getChannelsApi().getChannelByName(p.getId(), channel); 189 | } catch (Exception ex) { 190 | log.fatal(String.format("Retrieving channel name '%s' from project '%s' failed with message '%s'", 191 | channel, project, ex.getMessage())); 192 | success = false; 193 | } 194 | if (c == null) { 195 | log.fatal("Channel was not found."); 196 | success = false; 197 | } 198 | } 199 | // Check packageVersion 200 | String releaseNotesContent = ""; 201 | 202 | // Prepend Release Notes with Jenkins URL? 203 | // Do this regardless if Release Notes are specified 204 | if (releaseNotesJenkinsLinkback) { 205 | final String buildUrlVar = "${BUILD_URL}"; 206 | 207 | // Use env vars 208 | String resolvedBuildUrlVar = envInjector.injectEnvironmentVariableValues(buildUrlVar); 209 | releaseNotesContent = String.format("Created by: %s%n", 210 | resolvedBuildUrlVar, 211 | resolvedBuildUrlVar); 212 | } 213 | 214 | if (releaseNotes) { 215 | if (isReleaseNotesSourceFile()) { 216 | try { 217 | releaseNotesContent += getReleaseNotesFromFile(build, releaseNotesFile); 218 | } catch (Exception ex) { 219 | log.fatal(String.format("Unable to get file contents from release ntoes file! - %s", ex.getMessage())); 220 | success = false; 221 | } 222 | } else if (isReleaseNotesSourceScm()) { 223 | releaseNotesContent += getReleaseNotesFromScm(build); 224 | } else { 225 | log.fatal(String.format("Bad configuration: if using release notes, should have source of file or scm. Found '%s'", releaseNotesSource)); 226 | success = false; 227 | } 228 | } 229 | 230 | if (!success) { // Early exit 231 | return success; 232 | } 233 | 234 | Set selectedPackages = getCombinedPackageList(p.getId(), packageConfigs, envInjector.injectEnvironmentVariableValues(defaultPackageVersion), log, envInjector); 235 | 236 | try { 237 | // Sanitize the release notes in preparation for JSON 238 | releaseNotesContent = JSONSanitizer.getInstance().sanitize(releaseNotesContent); 239 | String channelId = null; 240 | if (c != null) { 241 | channelId = c.getId(); 242 | } 243 | String results = getApi().getReleasesApi().createRelease(p.getId(), releaseVersion, channelId, releaseNotesContent, selectedPackages); 244 | JSONObject json = (JSONObject)JSONSerializer.toJSON(results); 245 | String urlSuffix = json.getJSONObject("Links").getString("Web"); 246 | String url = getOctopusDeployServer().getUrl(); 247 | if (url.endsWith("/")) { 248 | url = url.substring(0, url.length() - 1); 249 | } 250 | log.info("Release created: \n\t" + url + urlSuffix); 251 | build.addAction(new BuildInfoSummary(BuildInfoSummary.OctopusDeployEventType.Release, url + urlSuffix)); 252 | } catch (Exception ex) { 253 | log.fatal("Failed to create release: " + ex.getMessage()); 254 | success = false; 255 | } 256 | 257 | if (success && deployThisRelease) { 258 | OctopusDeployDeploymentRecorder deployment = new OctopusDeployDeploymentRecorder(getServerId(), project, releaseVersion, environment, tenant, "", waitForDeployment); 259 | success = deployment.perform(build, launcher, listener); 260 | } 261 | 262 | return success; 263 | } 264 | 265 | /** 266 | * Write the startup header for the logs to show what our inputs are. 267 | * @param log The logger 268 | */ 269 | private void logStartHeader(Log log) { 270 | log.info("Started Octopus Release"); 271 | log.info("======================="); 272 | log.info("Project: " + project); 273 | log.info("Release Version: " + releaseVersion); 274 | if (channel != null && !channel.isEmpty()) { 275 | log.info("Channel: " + channel); 276 | } 277 | log.info("Include Release Notes?: " + releaseNotes); 278 | if (releaseNotes) { 279 | log.info("\tRelease Notes Source: " + releaseNotesSource); 280 | log.info("\tRelease Notes File: " + releaseNotesFile); 281 | } 282 | log.info("Deploy this Release?: " + deployThisRelease); 283 | if (deployThisRelease) { 284 | log.info("\tEnvironment: " + environment); 285 | log.info("\tWait for Deployment: " + waitForDeployment); 286 | } 287 | if (packageConfigs == null || packageConfigs.isEmpty()) { 288 | log.info("Package Configurations: none"); 289 | } else { 290 | log.info("Package Configurations:"); 291 | for (PackageConfiguration pc : packageConfigs) { 292 | log.info("\t" + pc.getPackageName() + "\t" + pc.getPackageReferenceName() + "\tv" + pc.getPackageVersion()); 293 | } 294 | } 295 | log.info("======================="); 296 | } 297 | 298 | /** 299 | * Gets a package list that is a combination of the default packages (taken from the Octopus template) 300 | * and the packages selected. Selected package version overwrite the default package version for a given package 301 | * @param projectId 302 | * @param selectedPackages 303 | * @param defaultPackageVersion 304 | * @return A set that combines the default packages and selected packages 305 | */ 306 | private Set getCombinedPackageList(String projectId, List selectedPackages, 307 | String defaultPackageVersion, Log log, EnvironmentVariableValueInjector envInjector) 308 | { 309 | Set combinedList = new HashSet<>(); 310 | 311 | //Get all selected package names for easier lookup later 312 | Map selectedNames = new HashMap<>(); 313 | if (selectedPackages != null) { 314 | for (PackageConfiguration pkgConfig : selectedPackages) { 315 | SelectedPackage sp = new SelectedPackage(envInjector.injectEnvironmentVariableValues(pkgConfig.getPackageName()), null, pkgConfig.getPackageReferenceName(), envInjector.injectEnvironmentVariableValues(pkgConfig.getPackageVersion())); 316 | selectedNames.put(envInjector.injectEnvironmentVariableValues(pkgConfig.getPackageName()), sp); 317 | combinedList.add(sp); 318 | } 319 | } 320 | 321 | DeploymentProcessTemplate defaultPackages = null; 322 | //If not default version specified, ignore all default packages 323 | try { 324 | defaultPackages = getApi().getDeploymentsApi().getDeploymentProcessTemplateForProject(projectId); 325 | } catch (Exception ex) { 326 | //Default package retrieval unsuccessful 327 | log.info(String.format("Could not retrieve default package list for project id: %s. No default packages will be used", projectId)); 328 | } 329 | 330 | if (defaultPackages != null) { 331 | for (SelectedPackage selPkg : defaultPackages.getSteps()) { 332 | String stepName = selPkg.getStepName(); 333 | String packageId = selPkg.getPackageId(); 334 | String packageReferenceName = selPkg.getPackageReferenceName(); 335 | 336 | //Only add if it was not a selected package 337 | if (!selectedNames.containsKey(stepName)) { 338 | //If packageId specified replace by stepName retrieved from DeploymentProcessTemplate 339 | //Emulates same behaviour as octo.client project https://octopus.com/docs/api-and-integration/octo.exe-command-line/creating-releases 340 | if (selectedNames.containsKey(packageId)) { 341 | SelectedPackage sc = selectedNames.get(packageId); 342 | sc.setStepName(stepName); 343 | } else { 344 | //Get the default version, if not specified, warn 345 | if (defaultPackageVersion != null && !defaultPackageVersion.isEmpty()) { 346 | combinedList.add(new SelectedPackage(stepName, null, packageReferenceName, defaultPackageVersion)); 347 | log.info(String.format("Using default version (%s) of package %s", defaultPackageVersion, stepName)); 348 | } else { 349 | log.error(String.format("Required package %s not included because package is not in Package Configuration list and no default package version defined", stepName)); 350 | } 351 | } 352 | } 353 | } 354 | } 355 | 356 | return combinedList; 357 | } 358 | 359 | /** 360 | * Return the release notes contents from a file. 361 | * @param build our build 362 | * @return string contents of file 363 | * @throws IOException if there was a file read io problem 364 | * @throws InterruptedException if the action for reading was interrupted 365 | */ 366 | private String getReleaseNotesFromFile(AbstractBuild build, String releaseNotesFilename) throws IOException, InterruptedException { 367 | FilePath path = new FilePath(build.getWorkspace(), releaseNotesFilename); 368 | return path.act(new ReadFileCallable()); 369 | } 370 | 371 | /** 372 | * This callable allows us to read files from other nodes - ie. Jenkins slaves. 373 | */ 374 | private static final class ReadFileCallable implements FileCallable { 375 | public final static String ERROR_READING = ""; 376 | 377 | @Override 378 | public String invoke(File f, VirtualChannel channel) { 379 | try { 380 | return StringUtils.join(Files.readAllLines(f.toPath(), StandardCharsets.UTF_8), "\n"); 381 | } catch (IOException ex) { 382 | return ERROR_READING; 383 | } 384 | } 385 | 386 | @Override 387 | public void checkRoles(RoleChecker rc) throws SecurityException { 388 | 389 | } 390 | } 391 | 392 | /** 393 | * Attempt to load release notes info from SCM. 394 | * @param build the jenkins build 395 | * @return release notes as a single string 396 | */ 397 | private String getReleaseNotesFromScm(AbstractBuild build) { 398 | StringBuilder notes = new StringBuilder(); 399 | AbstractProject project = build.getProject(); 400 | AbstractBuild lastSuccessfulBuild = (AbstractBuild)project.getLastSuccessfulBuild(); 401 | AbstractBuild currentBuild = null; 402 | if (lastSuccessfulBuild == null) { 403 | AbstractBuild lastBuild = (AbstractBuild)project.getLastBuild(); 404 | currentBuild = lastBuild; 405 | } 406 | else 407 | { 408 | currentBuild = lastSuccessfulBuild.getNextBuild(); 409 | } 410 | if (currentBuild != null) { 411 | while (currentBuild != build) 412 | { 413 | String currBuildNotes = convertChangeSetToString(currentBuild); 414 | if (!currBuildNotes.isEmpty()) { 415 | notes.append(currBuildNotes); 416 | } 417 | 418 | currentBuild = currentBuild.getNextBuild(); 419 | } 420 | // Also include the current build 421 | String currBuildNotes = convertChangeSetToString(build); 422 | if (!currBuildNotes.isEmpty()) { 423 | notes.append(currBuildNotes); 424 | } 425 | } 426 | 427 | return notes.toString(); 428 | } 429 | 430 | /** 431 | * Convert a build's change set to a string, each entry on a new line 432 | * @param build The build to poll changesets from 433 | * @return The changeset as a string 434 | */ 435 | private String convertChangeSetToString(AbstractBuild build) { 436 | StringBuilder allChangeNotes = new StringBuilder(); 437 | if (build != null) { 438 | ChangeLogSet changeSet = build.getChangeSet(); 439 | for (Object item : changeSet.getItems()) { 440 | ChangeLogSet.Entry entry = (ChangeLogSet.Entry) item; 441 | allChangeNotes.append(entry.getMsg()).append("\n"); 442 | } 443 | } 444 | return allChangeNotes.toString(); 445 | } 446 | 447 | /** 448 | * Descriptor for {@link OctopusDeployReleaseRecorder}. Used as a singleton. 449 | * The class is marked as public so that it can be accessed from views. 450 | */ 451 | @Extension // This indicates to Jenkins that this is an implementation of an extension point. 452 | public static final class DescriptorImpl extends BuildStepDescriptor { 453 | private static final String PROJECT_RELEASE_VALIDATION_MESSAGE = "Project must be set to validate release."; 454 | private static final String SERVER_ID_VALIDATION_MESSAGE = "Could not validate without a valid Server ID."; 455 | 456 | public boolean isApplicable(Class aClass) { 457 | return true; 458 | } 459 | 460 | @Override 461 | public String getDisplayName() { 462 | return "OctopusDeploy Release"; 463 | } 464 | 465 | @Override 466 | public boolean configure(StaplerRequest req, JSONObject formData) throws Descriptor.FormException { 467 | save(); 468 | return true; 469 | } 470 | 471 | private OctopusApi getApiByServerId(String serverId){ 472 | return AbstractOctopusDeployRecorder.getOctopusDeployServer(serverId).getApi(); 473 | } 474 | 475 | public String getDefaultOctopusDeployServerId() { 476 | OctopusDeployServer server = AbstractOctopusDeployRecorder.getDefaultOctopusDeployServer(); 477 | if(server != null){ 478 | return server.getId(); 479 | } 480 | return null; 481 | } 482 | 483 | /** 484 | * Check that the serverId field is not empty and does exist. 485 | * @param serverId The id of OctopusDeployServer in the configuration. 486 | * @return Ok if not empty, error otherwise. 487 | */ 488 | public FormValidation doCheckServerId(@QueryParameter String serverId) { 489 | serverId = serverId.trim(); 490 | return OctopusValidator.validateServerId(serverId); 491 | } 492 | 493 | /** 494 | * Check that the project field is not empty and represents an actual project. 495 | * @param project The name of the project. 496 | * @param serverId The id of OctopusDeployServer in the configuration. 497 | * @return FormValidation message if not ok. 498 | */ 499 | public FormValidation doCheckProject(@QueryParameter String project, @QueryParameter String serverId) { 500 | project = project.trim(); 501 | 502 | serverId = serverId.trim(); 503 | if (doCheckServerId(serverId).kind != FormValidation.Kind.OK) { 504 | return FormValidation.warning(SERVER_ID_VALIDATION_MESSAGE); 505 | } 506 | 507 | OctopusApi api = getApiByServerId(serverId); 508 | OctopusValidator validator = new OctopusValidator(api); 509 | return validator.validateProject(project); 510 | } 511 | 512 | /** 513 | * Check that the Channel field is either not set (default) or set to a real channel. 514 | * @param channel release channel. 515 | * @param project The name of the project. 516 | * @param serverId The id of OctopusDeployServer in the configuration. 517 | * @return Ok if not empty, error otherwise. 518 | */ 519 | public FormValidation doCheckChannel(@QueryParameter String channel, @QueryParameter String project, @QueryParameter String serverId) { 520 | channel = channel.trim(); 521 | project = project.trim(); 522 | 523 | serverId = serverId.trim(); 524 | if (doCheckServerId(serverId).kind != FormValidation.Kind.OK) { 525 | return FormValidation.warning(SERVER_ID_VALIDATION_MESSAGE); 526 | } 527 | 528 | OctopusApi api = getApiByServerId(serverId); 529 | OctopusValidator validator = new OctopusValidator(api); 530 | return validator.validateChannel(channel, project); 531 | } 532 | 533 | /** 534 | * Check that the releaseVersion field is not empty. 535 | * @param releaseVersion release version. 536 | * @param project The name of the project. 537 | * @param serverId The id of OctopusDeployServer in the configuration. 538 | * @return Ok if not empty, error otherwise. 539 | */ 540 | public FormValidation doCheckReleaseVersion(@QueryParameter String releaseVersion, @QueryParameter String project, @QueryParameter String serverId) { 541 | releaseVersion = releaseVersion.trim(); 542 | 543 | if (doCheckServerId(serverId).kind != FormValidation.Kind.OK) { 544 | return FormValidation.warning(SERVER_ID_VALIDATION_MESSAGE); 545 | } 546 | 547 | OctopusApi api = getApiByServerId(serverId); 548 | if (project == null || project.isEmpty()) { 549 | return FormValidation.warning(PROJECT_RELEASE_VALIDATION_MESSAGE); 550 | } 551 | com.octopusdeploy.api.data.Project p; 552 | try { 553 | p = api.getProjectsApi().getProjectByName(project); 554 | if (p == null) { 555 | return FormValidation.warning(PROJECT_RELEASE_VALIDATION_MESSAGE); 556 | } 557 | } catch (Exception ex) { 558 | return FormValidation.warning(PROJECT_RELEASE_VALIDATION_MESSAGE); 559 | } 560 | 561 | OctopusValidator validator = new OctopusValidator(api); 562 | return validator.validateRelease(releaseVersion, p.getId(), OctopusValidator.ReleaseExistenceRequirement.MustNotExist); 563 | } 564 | 565 | /** 566 | * Check that the releaseNotesFile field is not empty. 567 | * @param releaseNotesFile The path to the release notes file, relative to the WS. 568 | * @return Ok if not empty, error otherwise. 569 | */ 570 | public FormValidation doCheckReleaseNotesFile(@QueryParameter String releaseNotesFile) { 571 | if (releaseNotesFile.isEmpty()) { 572 | return FormValidation.error("Please provide a project notes file."); 573 | } 574 | 575 | return FormValidation.ok(); 576 | } 577 | 578 | /** 579 | * Check that the environment field is not empty, and represents a real environment. 580 | * @param environment The name of the environment. 581 | * @param serverId The id of OctopusDeployServer in the configuration. 582 | * @return FormValidation message if not ok. 583 | */ 584 | public FormValidation doCheckEnvironment(@QueryParameter String environment, @QueryParameter String serverId) { 585 | environment = environment.trim(); 586 | 587 | if (doCheckServerId(serverId).kind != FormValidation.Kind.OK) { 588 | return FormValidation.warning(SERVER_ID_VALIDATION_MESSAGE); 589 | } 590 | 591 | OctopusApi api = getApiByServerId(serverId); 592 | OctopusValidator validator = new OctopusValidator(api); 593 | return validator.validateEnvironment(environment); 594 | } 595 | 596 | /** 597 | * Data binding that returns all configured Octopus server ids to be used in the serverId drop-down list. 598 | * @return ComboBoxModel 599 | */ 600 | public ComboBoxModel doFillServerIdItems() { 601 | return new ComboBoxModel(getOctopusDeployServersIds()); 602 | } 603 | 604 | /** 605 | * Data binding that returns all possible environment names to be used in the environment autocomplete. 606 | * @param serverId The id of OctopusDeployServer in the configuration. 607 | * @return ComboBoxModel 608 | */ 609 | public ComboBoxModel doFillEnvironmentItems(@QueryParameter String serverId) { 610 | ComboBoxModel names = new ComboBoxModel(); 611 | 612 | if (doCheckServerId(serverId).kind != FormValidation.Kind.OK) { 613 | return names; 614 | } 615 | 616 | OctopusApi api = getApiByServerId(serverId); 617 | try { 618 | Set environments = api.getEnvironmentsApi().getAllEnvironments(); 619 | for (com.octopusdeploy.api.data.Environment env : environments) { 620 | names.add(env.getName()); 621 | } 622 | } catch (Exception ex) { 623 | Logger.getLogger(OctopusDeployReleaseRecorder.class.getName()).log(Level.SEVERE, "Filling environments combo failed!", ex); 624 | } 625 | return names; 626 | } 627 | 628 | /** 629 | * Data binding that returns all possible tenant names to be used in the tenant autocomplete. 630 | * @param serverId The id of OctopusDeployServer in the configuration. 631 | * @return ComboBoxModel 632 | */ 633 | public ComboBoxModel doFillTenantItems(@QueryParameter String serverId) { 634 | ComboBoxModel names = new ComboBoxModel(); 635 | 636 | if (doCheckServerId(serverId).kind != FormValidation.Kind.OK) { 637 | return names; 638 | } 639 | 640 | OctopusApi api = getApiByServerId(serverId); 641 | try { 642 | Set tenants = api.getTenantsApi().getAllTenants(); 643 | for (com.octopusdeploy.api.data.Tenant ten : tenants) { 644 | names.add(ten.getName()); 645 | } 646 | } catch (Exception ex) { 647 | Logger.getLogger(OctopusDeployDeploymentRecorder.class.getName()).log(Level.SEVERE, null, ex); 648 | } 649 | return names; 650 | } 651 | 652 | /** 653 | * Data binding that returns all possible project names to be used in the project autocomplete. 654 | * @param serverId The id of OctopusDeployServer in the configuration. 655 | * @return ComboBoxModel 656 | */ 657 | public ComboBoxModel doFillProjectItems(@QueryParameter String serverId) { 658 | ComboBoxModel names = new ComboBoxModel(); 659 | 660 | if (doCheckServerId(serverId).kind != FormValidation.Kind.OK) { 661 | return names; 662 | } 663 | 664 | OctopusApi api = getApiByServerId(serverId); 665 | try { 666 | Set projects = api.getProjectsApi().getAllProjects(); 667 | for (com.octopusdeploy.api.data.Project proj : projects) { 668 | names.add(proj.getName()); 669 | } 670 | } catch (Exception ex) { 671 | Logger.getLogger(OctopusDeployReleaseRecorder.class.getName()).log(Level.SEVERE, "Filling projects combo failed!", ex); 672 | } 673 | return names; 674 | } 675 | 676 | /** 677 | * Data binding that returns all possible channels names to be used in the channel autocomplete. 678 | * @param project the project name 679 | * @param serverId The id of OctopusDeployServer in the configuration. 680 | * @return ComboBoxModel 681 | */ 682 | public ComboBoxModel doFillChannelItems(@QueryParameter String project, @QueryParameter String serverId) { 683 | ComboBoxModel names = new ComboBoxModel(); 684 | 685 | if (doCheckServerId(serverId).kind != FormValidation.Kind.OK) { 686 | return names; 687 | } 688 | 689 | OctopusApi api = getApiByServerId(serverId); 690 | if (project != null && !project.isEmpty()) { 691 | try { 692 | com.octopusdeploy.api.data.Project p = api.getProjectsApi().getProjectByName(project); 693 | if (p != null) { 694 | Set channels = api.getChannelsApi().getChannelsByProjectId(p.getId()); 695 | for (com.octopusdeploy.api.data.Channel channel : channels) { 696 | names.add(channel.getName()); 697 | } 698 | } 699 | } catch (Exception ex) { 700 | Logger.getLogger(OctopusDeployReleaseRecorder.class.getName()).log(Level.SEVERE, "Filling Channel combo failed!", ex); 701 | } 702 | } 703 | return names; 704 | } 705 | } 706 | } 707 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/octopusdeploy/OctopusDeployServer.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.octopusdeploy; 2 | 3 | import com.octopusdeploy.api.OctopusApi; 4 | import org.kohsuke.stapler.DataBoundConstructor; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * @author wbenayed 10 | */ 11 | public class OctopusDeployServer implements Serializable { 12 | // This value should be incremented every time that this serializable's contract changes 13 | private static final long serialVersionUID = 1; 14 | 15 | private final boolean isDefault; 16 | public boolean isDefault() { 17 | return isDefault; 18 | } 19 | 20 | private String id; 21 | public String getId() { 22 | return id; 23 | } 24 | 25 | private String url; 26 | public String getUrl() { 27 | return url; 28 | } 29 | 30 | private String apiKey; 31 | public String getApiKey() { 32 | return apiKey; 33 | } 34 | 35 | private transient OctopusApi api; 36 | public OctopusApi getApi() { 37 | ///TODO use better approach to achieve Laziness 38 | if (api == null) { 39 | api = new OctopusApi(url, apiKey); 40 | } 41 | return api; 42 | } 43 | 44 | public OctopusDeployServer(String serverId, String url, String apiKey, boolean isDefault) { 45 | this.id = serverId.trim(); 46 | this.url = url.trim(); 47 | this.apiKey = apiKey.trim(); 48 | this.isDefault = isDefault; 49 | } 50 | 51 | @DataBoundConstructor 52 | public OctopusDeployServer(String serverId, String url, String apiKey) { 53 | this(serverId, url, apiKey, false); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/octopusdeploy/OctopusValidator.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.octopusdeploy; 2 | 3 | import com.octopusdeploy.api.data.Release; 4 | import com.octopusdeploy.api.*; 5 | import hudson.util.FormValidation; 6 | import java.io.IOException; 7 | import java.util.List; 8 | import java.util.Set; 9 | 10 | /** 11 | * Validations on input for OctopusDeploy. 12 | */ 13 | public class OctopusValidator { 14 | private final OctopusApi api; 15 | 16 | public OctopusValidator(OctopusApi api) { 17 | this.api = api; 18 | } 19 | 20 | /** 21 | * Provides validation on a Project. 22 | * Validates: 23 | * Project is not empty. 24 | * Project exists in Octopus. 25 | * Project is appropriate case. 26 | * @param projectName name of the project to validate. 27 | * @return a form validation. 28 | */ 29 | public FormValidation validateProject(String projectName) { 30 | if (projectName.isEmpty()) { 31 | return FormValidation.error("Please provide a project name."); 32 | } 33 | try { 34 | com.octopusdeploy.api.data.Project p = api.getProjectsApi().getProjectByName(projectName, true); 35 | if (p == null) 36 | { 37 | return FormValidation.error("Project not found."); 38 | } 39 | if (!projectName.equals(p.getName())) 40 | { 41 | return FormValidation.warning("Project name case does not match. Did you mean '%s'?", p.getName()); 42 | } 43 | } catch (IllegalArgumentException ex) { 44 | return FormValidation.error(ex.getMessage()); 45 | } catch (IOException ex) { 46 | return FormValidation.error(ex.getMessage()); 47 | } 48 | return FormValidation.ok(); 49 | } 50 | 51 | /** 52 | * Provides validation on a Channel. 53 | * Validates: 54 | * Project is not empty. 55 | * Project exists in Octopus. 56 | * Project is appropriate case. 57 | * Channel is either empty or exists in Octopus 58 | * @param channelName name of the channel to validate 59 | * @param projectName name of the project to validate. 60 | * @return a form validation. 61 | */ 62 | public FormValidation validateChannel(String channelName, String projectName) { 63 | if (channelName != null && !channelName.isEmpty()) { 64 | if (projectName == null || projectName.isEmpty()) { 65 | return FormValidation.warning("Project must be set to validate this field."); 66 | } 67 | com.octopusdeploy.api.data.Project project; 68 | com.octopusdeploy.api.data.Channel channel; 69 | try { 70 | project = api.getProjectsApi().getProjectByName(projectName); 71 | if (project != null) { 72 | channel = api.getChannelsApi().getChannelByName(project.getId(), channelName); 73 | if (channel == null) { 74 | return FormValidation.error("Channel not found."); 75 | } 76 | } 77 | else 78 | { 79 | return FormValidation.warning("Project must be set to validate this field."); 80 | } 81 | } catch (IllegalArgumentException ex) { 82 | return FormValidation.warning("Unable to validate field - " + ex.getMessage()); 83 | } catch (IOException ex) { 84 | return FormValidation.warning("Unable to validate field - " + ex.getMessage()); 85 | } 86 | } 87 | return FormValidation.ok(); 88 | } 89 | 90 | /** 91 | * Provides validation on an environment. 92 | * Validates: 93 | * Environment is not empty. 94 | * Environment exists in Octopus. 95 | * Environment is appropriate case. 96 | * @param environmentName the name of the environment to validate. 97 | * @return a form validation. 98 | */ 99 | public FormValidation validateEnvironment(String environmentName) { 100 | if (environmentName.isEmpty()) { 101 | return FormValidation.error("Please provide an environment name."); 102 | } 103 | try { 104 | com.octopusdeploy.api.data.Environment env = api.getEnvironmentsApi().getEnvironmentByName(environmentName, true); 105 | if (env == null) 106 | { 107 | return FormValidation.error("Environment not found."); 108 | } 109 | if (!environmentName.equals(env.getName())) 110 | { 111 | return FormValidation.warning("Environment name case does not match. Did you mean '%s'?", env.getName()); 112 | } 113 | } catch (IllegalArgumentException ex) { 114 | return FormValidation.error(ex.getMessage()); 115 | } catch (IOException ex) { 116 | return FormValidation.error(ex.getMessage()); 117 | } 118 | return FormValidation.ok(); 119 | } 120 | 121 | /** 122 | * Provides validation on releases. 123 | * Validates: 124 | * The project is set. 125 | * The release is not empty. 126 | * The release conforms to the existence check requirement. 127 | * @param releaseVersion the release version. 128 | * @param projectId the project's Id that this release is for. 129 | * @param existanceCheckReq the requirement for the existence of the release. 130 | * @return FormValidation response 131 | */ 132 | public FormValidation validateRelease(String releaseVersion, String projectId, ReleaseExistenceRequirement existanceCheckReq) { 133 | if (releaseVersion.isEmpty()) { 134 | return FormValidation.error("Please provide a release version."); 135 | } 136 | try { 137 | Set releases = api.getReleasesApi().getReleasesForProject(projectId); 138 | boolean found = false; 139 | for (Release release : releases) { 140 | if (releaseVersion.equals(release.getVersion()) ) { 141 | found = true; 142 | break; 143 | } 144 | } 145 | if (found && existanceCheckReq == ReleaseExistenceRequirement.MustNotExist) { 146 | return FormValidation.error("Release %s already exists for project %s!", releaseVersion, projectId); 147 | } 148 | if (!found && existanceCheckReq == ReleaseExistenceRequirement.MustExist) { 149 | return FormValidation.error("Release %s doesn't exist for project %s!", releaseVersion, projectId); 150 | } 151 | } catch (IllegalArgumentException ex) { 152 | return FormValidation.error(ex.getMessage()); 153 | } catch (IOException ex) { 154 | return FormValidation.error(ex.getMessage()); 155 | } 156 | return FormValidation.ok(); 157 | } 158 | 159 | public static FormValidation validateServerId(String serverId) { 160 | if (serverId==null || serverId.isEmpty()) { 161 | return FormValidation.error("Please set a Server Id"); 162 | } 163 | if(serverId.equals("default")) { 164 | return FormValidation.ok(); 165 | } 166 | List ids = AbstractOctopusDeployRecorder.getOctopusDeployServersIds(); 167 | if (ids.isEmpty()){ 168 | return FormValidation.error("There are no OctopusDeploy servers configured."); 169 | } 170 | if (!ids.contains(serverId)) { 171 | return FormValidation.error("There are no OctopusDeploy servers configured with this Server ID."); 172 | } 173 | return FormValidation.ok(); 174 | } 175 | 176 | /** 177 | * Whether or not a release must exist or must not exist depending on the operation being done. 178 | */ 179 | public enum ReleaseExistenceRequirement { 180 | MustExist, MustNotExist 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/hudson/plugins/octopusdeploy/PackageConfiguration.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.octopusdeploy; 2 | 3 | import hudson.Extension; 4 | import hudson.model.*; 5 | import java.io.Serializable; 6 | import org.kohsuke.stapler.*; 7 | import org.kohsuke.stapler.export.*; 8 | 9 | /** 10 | * A data object representing a package configuration. 11 | * @author cwetherby 12 | */ 13 | @ExportedBean 14 | public class PackageConfiguration extends AbstractDescribableImpl implements Serializable { 15 | /** 16 | * The name of the package. 17 | */ 18 | private final String packageName; 19 | @Exported 20 | public String getPackageName() { 21 | return packageName; 22 | } 23 | 24 | /** 25 | * The package reference's name. 26 | */ 27 | private final String packageReferenceName; 28 | @Exported 29 | public String getPackageReferenceName() { 30 | return packageReferenceName; 31 | } 32 | 33 | /** 34 | * The version of the package to use. 35 | */ 36 | private final String packageVersion; 37 | @Exported 38 | public String getPackageVersion() { 39 | return packageVersion; 40 | } 41 | 42 | @DataBoundConstructor 43 | public PackageConfiguration(String packageName, String packageReferenceName, String packageVersion) { 44 | this.packageName = packageName.trim(); 45 | this.packageReferenceName = packageReferenceName.trim(); 46 | this.packageVersion = packageVersion.trim(); 47 | } 48 | 49 | @Extension 50 | public static class DescriptorImpl extends Descriptor { 51 | @Override 52 | public String getDisplayName() { 53 | return ""; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/BuildInfoSummary/badge.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | OctopusDeploy 4 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/BuildInfoSummary/summary.jelly: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | ${it.displayName} 6 | 7 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployDeploymentRecorder/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployDeploymentRecorder/global.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployDeploymentRecorder/help-environment.html: -------------------------------------------------------------------------------- 1 |
2 | The environment to deploy to. 3 |
4 | This field is case-sensitive. 5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployDeploymentRecorder/help-project.html: -------------------------------------------------------------------------------- 1 |
2 | The project name in OctopusDeploy. 3 |
4 | This field is case-sensitive. 5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployDeploymentRecorder/help-releaseVersion.html: -------------------------------------------------------------------------------- 1 |
2 | The version of the release to move. 3 |
4 | This field is case-sensitive. 5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployDeploymentRecorder/help-serverId.html: -------------------------------------------------------------------------------- 1 |
2 | The identifier of the OctopusDeploy server which you want to use for executing this deployment. 3 |
4 | Server Id is set in the global OctopusDeploy Plugin configuration. 5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployDeploymentRecorder/help-tenant.html: -------------------------------------------------------------------------------- 1 |
2 | The tenant to deploy to. 3 |
4 | This field is case-sensitive. 5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployDeploymentRecorder/help-variables.html: -------------------------------------------------------------------------------- 1 |
2 | List of variable to pass to the Depoyment process 3 |
4 | Use Java properties notation 5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployDeploymentRecorder/help-waitForDeployment.html: -------------------------------------------------------------------------------- 1 |
2 | If selected, this build will wait until the deployment is complete. 3 |
4 | When this is selected, the deployment state will fail the build if the state ends as "Failed" 5 |
-------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployPlugin/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployPlugin/global.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployPlugin/help-apiKey.html: -------------------------------------------------------------------------------- 1 |
2 | The API key for a user intended to interact with the OctopusDeploy instance. 3 |
4 | This key will be used for all operations with OctopusDeploy on this server for this plugin. 5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployPlugin/help-serverId.html: -------------------------------------------------------------------------------- 1 |
2 | Server Id is unique identifier given to each Octopus Deploy server. It is recommended that this is easy for users to recognize. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployPlugin/help-url.html: -------------------------------------------------------------------------------- 1 |
2 | The OctopusDeploy host URL. 3 |
4 | For example: https://OctopusDeploy.mydomain.net 5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployReleaseRecorder/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 |
68 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployReleaseRecorder/global.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployReleaseRecorder/help-channel.html: -------------------------------------------------------------------------------- 1 |
2 | If left empty the Default channel for the project will be used. 3 |
4 | Otherwise, specify a channel by name. The channel specified must exist for the current project. 5 |
-------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployReleaseRecorder/help-defaultPackageVersion.html: -------------------------------------------------------------------------------- 1 |
2 | The default version to use for all required packages that are not specified in Package Configurations. No default packages are used if a default version is not specified 3 |
-------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployReleaseRecorder/help-deployThisRelease.html: -------------------------------------------------------------------------------- 1 |
2 | Select this when this release should be deployed immediately after it is created. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployReleaseRecorder/help-environment.html: -------------------------------------------------------------------------------- 1 |
2 | The environment to deploy this release into. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployReleaseRecorder/help-jenkinsUrlLinkback.html: -------------------------------------------------------------------------------- 1 |
2 | Adds a linkback to the Jenkins build that created the release in the Octopus Deploy release notes 3 |
-------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployReleaseRecorder/help-project.html: -------------------------------------------------------------------------------- 1 |
2 | The name of the project as defined in OctopusDeploy.
3 | This field is case sensitive. 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployReleaseRecorder/help-releaseNotes.html: -------------------------------------------------------------------------------- 1 |
2 | Select this option when you want to include release notes in this release. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployReleaseRecorder/help-releaseNotesFile.html: -------------------------------------------------------------------------------- 1 |
2 | Path to file, relative from workspace root, to read for release notes. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployReleaseRecorder/help-releaseNotesFromSCM.html: -------------------------------------------------------------------------------- 1 |
2 | Get the release notes from SCM. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployReleaseRecorder/help-releaseVersion.html: -------------------------------------------------------------------------------- 1 |
2 | The version of the release to create. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployReleaseRecorder/help-serverId.html: -------------------------------------------------------------------------------- 1 |
2 | The identifier of the OctopusDeploy server which you want to use for creating this release. 3 |
4 | Server Id is set in the global OctopusDeploy Plugin configuration. 5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployReleaseRecorder/help-tenant.html: -------------------------------------------------------------------------------- 1 |
2 | The tenant to deploy to. 3 |
4 | This field is case-sensitive. 5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/OctopusDeployReleaseRecorder/help-waitForDeployment.html: -------------------------------------------------------------------------------- 1 |
2 | If selected, this build will wait until the deployment is complete. 3 |
4 | When this is selected, the deployment state will fail the build if the state ends as "Failed" 5 |
-------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/PackageConfiguration/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/PackageConfiguration/global.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/PackageConfiguration/help-packageName.html: -------------------------------------------------------------------------------- 1 |
2 | The step-name that requires a version for a nuget package. 3 |
4 | This field is case-sensitive. 5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/PackageConfiguration/help-packageReferenceName.html: -------------------------------------------------------------------------------- 1 |
2 | The package reference name within the step. This value is required only if you are using the package referencing feature. 3 |
4 | This field is case-sensitive. 5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/hudson/plugins/octopusdeploy/PackageConfiguration/help-packageVersion.html: -------------------------------------------------------------------------------- 1 |
2 | The package's version for this release. 3 |
4 | This field is case-sensitive. 5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 5 |
6 | This plugin allows integration with one or more OctopusDeploy instances. 7 |
8 | -------------------------------------------------------------------------------- /src/main/webapp/images/octopus-d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/octopusdeploy-plugin/247a2cc99fc57e3ecc6d718855a634772cb6d259/src/main/webapp/images/octopus-d.png -------------------------------------------------------------------------------- /src/main/webapp/images/octopus-o.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/octopusdeploy-plugin/247a2cc99fc57e3ecc6d718855a634772cb6d259/src/main/webapp/images/octopus-o.png -------------------------------------------------------------------------------- /src/main/webapp/images/octopus-r.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/octopusdeploy-plugin/247a2cc99fc57e3ecc6d718855a634772cb6d259/src/main/webapp/images/octopus-r.png -------------------------------------------------------------------------------- /src/test/java/com/octopusdeploy/api/ErrorParserTest.java: -------------------------------------------------------------------------------- 1 | package com.octopusdeploy.api; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashSet; 5 | import java.util.List; 6 | import java.util.Random; 7 | import java.util.Set; 8 | import org.junit.After; 9 | import org.junit.AfterClass; 10 | import org.junit.Before; 11 | import org.junit.BeforeClass; 12 | import org.junit.Test; 13 | import static org.junit.Assert.*; 14 | 15 | /** 16 | * OctopusApi tests 17 | * @author jlabroad 18 | */ 19 | public class ErrorParserTest { 20 | 21 | private Random rand = null; 22 | private Set forbiddenChars = null; 23 | 24 | public ErrorParserTest() { 25 | } 26 | 27 | @BeforeClass 28 | public static void setUpClass() { 29 | } 30 | 31 | @AfterClass 32 | public static void tearDownClass() { 33 | } 34 | 35 | @Before 36 | public void setUp() { 37 | rand = new Random(); 38 | 39 | //These characters cannot be in error messages. 40 | forbiddenChars = new HashSet(); 41 | forbiddenChars.add('\"'); 42 | forbiddenChars.add('\\'); 43 | forbiddenChars.add(']'); 44 | } 45 | 46 | @After 47 | public void tearDown() { 48 | } 49 | 50 | /** 51 | * Test of getErrorsFromResponse method, of class OctopusApi. Using a single sample Octopus response 52 | */ 53 | @Test 54 | public void testGetErrorsFromResponseStatic() { 55 | System.out.println("getErrorsFromResponse"); 56 | String errMsg1 = "No package version was specified for the step 'test nuget'"; 57 | String errMsg2 = "Error msg number 2"; 58 | String response = String.format(" Octopus Deploy
\"Octoclippy

Oops!

Something went wrong...

Show details
", 59 | errMsg1, errMsg2); 60 | String result = ErrorParser.getErrorsFromResponse(response); 61 | assertEquals(result.contains(errMsg1), true); 62 | assertEquals(result.contains(errMsg2), true); 63 | } 64 | 65 | /** 66 | * Test of getErrorsFromResponse method, of class OctopusApi. 67 | * Using many semi-random Octopus responses 68 | */ 69 | @Test 70 | public void testGetErrorsFromResponseRandom() { 71 | for (int i = 0; i < 2000; i++) { 72 | List errMsgs = new ArrayList(); 73 | String response = generateRandomErrorResponse(errMsgs); 74 | String result = ErrorParser.getErrorsFromResponse(response); 75 | for (String errMsg : errMsgs) { 76 | boolean testResult = result.contains(errMsg); 77 | if (!testResult) { 78 | System.out.println(String.format("Could not find error msg: %s", errMsg)); 79 | } 80 | assertEquals(testResult, true); 81 | } 82 | } 83 | } 84 | 85 | /** 86 | * Generate a random error response 87 | * @param errorMsgList 88 | * @return 89 | */ 90 | private String generateRandomErrorResponse(List errorMsgList) { 91 | final int maxNumMsgs = 15; 92 | String errMsg = ""; 93 | String prefix = generateRandomString(); 94 | String suffix = generateRandomString(); 95 | 96 | int numDetails = (int)(rand.nextDouble() * maxNumMsgs); 97 | String errorFormatPrefix = "var errorData = {\"title\":\"Bad request\",\"message\":\"There was a problem with your request.\",\"details\":{\"ErrorMessage\":\"There was a problem with your request.\",\"Errors\":["; 98 | String errorFormatSuffix = "]}};"; 99 | errMsg += prefix; 100 | errMsg += errorFormatPrefix; 101 | for (int i = 0; i < numDetails; i++) { 102 | String randomErrMsg = generateRandomString(); 103 | errMsg += String.format("\"%s\"", randomErrMsg); 104 | errorMsgList.add(randomErrMsg); 105 | if (i < numDetails - 1) 106 | errMsg += ", "; 107 | } 108 | errMsg += errorFormatSuffix; 109 | errMsg += suffix; 110 | return errMsg; 111 | } 112 | 113 | private String generateRandomString() { 114 | final int maxCharacters = 500; 115 | int numCharacters = (int)(rand.nextDouble()*maxCharacters); 116 | 117 | final int minAscii = 32; 118 | final int maxAscii = 126; 119 | String msg = ""; 120 | for (int i = 0; i < numCharacters; i++) { 121 | int randInt = (int)(rand.nextDouble()*(maxAscii - minAscii) + minAscii + 0.5); 122 | Character randChar = (char)randInt; 123 | //Do not allow forbidden characters 124 | if (forbiddenChars.contains(randChar)) 125 | continue; //Just skip it 126 | 127 | msg += (char) randInt; 128 | } 129 | return msg; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/test/java/hudson/plugins/octopusdeploy/JSONSanitizerTest.java: -------------------------------------------------------------------------------- 1 | package hudson.plugins.octopusdeploy; 2 | 3 | import org.junit.After; 4 | import org.junit.AfterClass; 5 | import org.junit.Before; 6 | import org.junit.BeforeClass; 7 | import org.junit.Test; 8 | import static org.junit.Assert.*; 9 | 10 | /** 11 | * Unit tests for JSONSanitizer 12 | * @author jlabroad 13 | */ 14 | public class JSONSanitizerTest { 15 | 16 | public JSONSanitizerTest() { 17 | } 18 | 19 | @BeforeClass 20 | public static void setUpClass() { 21 | } 22 | 23 | @AfterClass 24 | public static void tearDownClass() { 25 | } 26 | 27 | @Before 28 | public void setUp() { 29 | } 30 | 31 | @After 32 | public void tearDown() { 33 | } 34 | 35 | /** 36 | * Test of sanitize method, of class JSONSanitizer. 37 | */ 38 | @Test 39 | public void testLargeString() { 40 | String testString = "These release notes include quotes and some special characters.\n" + 41 | "Consider this: \"I am a quote\" -anonymous, or \"\"I am a double-quote\" -anonymous\" -some other guy\n" + 42 | "Sometimes you have some \"quotes\", sometimes some other characters like ! @ # $ % ^ & * () - + = _ {} [] ~ `\n" + 43 | "Backslashes too: C:\\Program Files (x86)\\Jenkins\\workspace or \"C:\\Program Files (x86)\\Jenkins\\workspace\"\n" + 44 | "\\\\ 2 backslashes\n" + 45 | " This paragraph starts with a tab. This paragraph starts with a tab. This paragraph starts with a tab.\n" + 46 | "This paragraph starts with a tab. This paragraph starts with a tab. This paragraph starts with a tab.\n"; 47 | 48 | final String answer = "These release notes include quotes and some special characters.
" + 49 | "Consider this: \\\"I am a quote\\\" -anonymous, or \\\"\\\"I am a double-quote\\\" -anonymous\\\" -some other guy
" + 50 | "Sometimes you have some \\\"quotes\\\", sometimes some other characters like ! @ # $ % ^ & * () - + = _ {} [] ~ `
" + 51 | "Backslashes too: C:\\u005CProgram Files (x86)\\u005CJenkins\\u005Cworkspace or \\\"C:\\u005CProgram Files (x86)\\u005CJenkins\\u005Cworkspace\\\"
\\u005C\\u005C 2 backslashes
" + 52 | "    This paragraph starts with a tab. This paragraph starts with a tab. " + 53 | "This paragraph starts with a tab.
This paragraph starts with a tab. This paragraph starts with a tab. This paragraph starts with a tab.
"; 54 | 55 | String sanitized = JSONSanitizer.getInstance().sanitize(testString); 56 | assertEquals(sanitized.equals(answer), true); 57 | } 58 | } 59 | --------------------------------------------------------------------------------