├── .gitignore ├── src └── main │ ├── resources │ ├── index.jelly │ └── com │ │ └── podio │ │ └── hudson │ │ └── PodioBuildNotifier │ │ ├── config.jelly │ │ └── global.jelly │ ├── webapp │ └── help-globalConfig.html │ └── java │ └── com │ └── podio │ └── hudson │ └── PodioBuildNotifier.java ├── .settings ├── org.eclipse.jdt.core.prefs └── org.maven.ide.eclipse.prefs ├── .classpath ├── .project ├── README.md ├── LICENSE └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /work 3 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 |
2 | Plugin for posting build results to Podio 3 |
-------------------------------------------------------------------------------- /src/main/webapp/help-globalConfig.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | See help-projectConfig.html for more about what these HTMLs do. 4 |

5 |
-------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | #Fri Oct 22 17:00:20 CEST 2010 2 | eclipse.preferences.version=1 3 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 4 | org.eclipse.jdt.core.compiler.compliance=1.5 5 | org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning 6 | org.eclipse.jdt.core.compiler.source=1.5 7 | -------------------------------------------------------------------------------- /.settings/org.maven.ide.eclipse.prefs: -------------------------------------------------------------------------------- 1 | #Fri Oct 22 16:54:12 CEST 2010 2 | activeProfiles= 3 | eclipse.preferences.version=1 4 | fullBuildGoals=process-test-resources 5 | includeModules=false 6 | resolveWorkspaceProjects=true 7 | resourceFilterGoals=process-resources resources\:testResources 8 | skipCompilerPlugin=true 9 | version=1 10 | -------------------------------------------------------------------------------- /src/main/resources/com/podio/hudson/PodioBuildNotifier/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | podio-build 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | org.eclipse.m2e.core.maven2Builder 13 | 14 | 15 | 16 | org.eclipse.jdt.core.javanature 17 | org.eclipse.m2e.core.maven2Nature 18 | 19 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Podio Build Notifier 2 | 3 | A [Jenkins](http://jenkins-ci.org/) post-build plugin for notifying a Podio app on the build status of a project. 4 | 5 | ## Requirements 6 | 7 | * [Java 7](http://java.com) 8 | * [Maven](http://maven.apache.org) 2+ 9 | 10 | ## Building 11 | 12 | $ mvn 13 | 14 | After the build finishes, the plugin is located in `target/podio-build-notifier.hpi`. 15 | 16 | ## Installation 17 | 18 | The plugin is not yet in any central repository, so head to the *Plugin Manager* in your Jenkins, go to the *Advanced* tab, and upload the `podio-build-notifier.hpi` file. 19 | 20 | ## Setup 21 | 22 | TBD 23 | 24 | ## License 25 | 26 | MIT 27 | -------------------------------------------------------------------------------- /src/main/resources/com/podio/hudson/PodioBuildNotifier/global.jelly: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is released under the MIT license: 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | org.jenkins-ci.plugins 6 | plugin 7 | 1.445 8 | 9 | 10 | com.podio 11 | podio-build-notifier 12 | Podio Build Notifier 13 | 1.2.2 14 | hpi 15 | 16 | 18 | 19 | 20 | repo.jenkins-ci.org 21 | http://repo.jenkins-ci.org/public/ 22 | 23 | 24 | 25 | 26 | MIT 27 | http://creativecommons.org/licenses/MIT/ 28 | 29 | 30 | 31 | 32 | 33 | repo.jenkins-ci.org 34 | http://repo.jenkins-ci.org/public/ 35 | 36 | 37 | 38 | 39 | com.podio 40 | api 41 | 0.7.4 42 | jar 43 | compile 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/main/java/com/podio/hudson/PodioBuildNotifier.java: -------------------------------------------------------------------------------- 1 | package com.podio.hudson; 2 | 3 | import hudson.Extension; 4 | import hudson.Launcher; 5 | import hudson.model.BuildListener; 6 | import hudson.model.Result; 7 | import hudson.model.AbstractBuild; 8 | import hudson.model.AbstractProject; 9 | import hudson.model.Run; 10 | import hudson.model.User; 11 | import hudson.scm.ChangeLogSet; 12 | import hudson.scm.ChangeLogSet.Entry; 13 | import hudson.tasks.BuildStepDescriptor; 14 | import hudson.tasks.BuildStepMonitor; 15 | import hudson.tasks.Notifier; 16 | import hudson.tasks.Publisher; 17 | import hudson.tasks.Mailer; 18 | import hudson.tasks.Mailer.UserProperty; 19 | import hudson.tasks.junit.CaseResult; 20 | import hudson.tasks.test.AbstractTestResultAction; 21 | import hudson.util.FormValidation; 22 | 23 | import java.io.IOException; 24 | import java.util.ArrayList; 25 | import java.util.Collections; 26 | import java.util.HashSet; 27 | import java.util.List; 28 | import java.util.Map; 29 | import java.util.Set; 30 | import java.util.logging.Logger; 31 | 32 | import javax.servlet.ServletException; 33 | 34 | import net.sf.json.JSONObject; 35 | 36 | import org.apache.commons.lang.StringUtils; 37 | import org.joda.time.LocalDate; 38 | import org.kohsuke.stapler.DataBoundConstructor; 39 | import org.kohsuke.stapler.QueryParameter; 40 | import org.kohsuke.stapler.StaplerRequest; 41 | 42 | import com.podio.APIFactory; 43 | import com.podio.ResourceFactory; 44 | import com.podio.app.AppAPI; 45 | import com.podio.app.Application; 46 | import com.podio.common.Reference; 47 | import com.podio.common.ReferenceType; 48 | import com.podio.contact.ContactAPI; 49 | import com.podio.contact.ProfileField; 50 | import com.podio.contact.ProfileMini; 51 | import com.podio.contact.ProfileType; 52 | import com.podio.item.FieldValuesUpdate; 53 | import com.podio.item.ItemAPI; 54 | import com.podio.item.ItemCreate; 55 | import com.podio.item.ItemsResponse; 56 | import com.podio.oauth.OAuthClientCredentials; 57 | import com.podio.oauth.OAuthUsernameCredentials; 58 | import com.podio.task.Task; 59 | import com.podio.task.TaskAPI; 60 | import com.podio.task.TaskCreate; 61 | import com.podio.task.TaskStatus; 62 | import com.podio.user.UserAPI; 63 | import com.sun.jersey.api.client.UniformInterfaceException; 64 | 65 | public class PodioBuildNotifier extends Notifier { 66 | 67 | @SuppressWarnings("unused") 68 | private static final Logger LOGGER = Logger 69 | .getLogger(PodioBuildNotifier.class.getName()); 70 | 71 | private final String appId; 72 | 73 | @DataBoundConstructor 74 | public PodioBuildNotifier(String appId) { 75 | this.appId = appId; 76 | } 77 | 78 | public String getAppId() { 79 | return appId; 80 | } 81 | 82 | private APIFactory getBaseAPI() { 83 | DescriptorImpl descriptor = (DescriptorImpl) getDescriptor(); 84 | 85 | return new APIFactory(new ResourceFactory(new OAuthClientCredentials( 86 | descriptor.clientId, descriptor.clientSecret), 87 | new OAuthUsernameCredentials(descriptor.username, 88 | descriptor.password))); 89 | } 90 | 91 | public BuildStepMonitor getRequiredMonitorService() { 92 | return BuildStepMonitor.BUILD; 93 | } 94 | 95 | @Override 96 | public boolean perform(AbstractBuild build, Launcher launcher, 97 | BuildListener listener) throws InterruptedException, IOException { 98 | APIFactory apiFactory = getBaseAPI(); 99 | 100 | String result = StringUtils.capitalize(build.getResult().toString() 101 | .toLowerCase()); 102 | result = result.replace('_', ' '); 103 | int spaceId = getSpace(apiFactory); 104 | String url = Mailer.descriptor().getUrl() + build.getParent().getUrl() 105 | + build.getNumber(); 106 | Set profiles = getProfiles(apiFactory, spaceId, build); 107 | 108 | Integer totalTestCases = null; 109 | Integer failedTestCases = null; 110 | AbstractTestResultAction testResult = build.getTestResultAction(); 111 | if (testResult != null) { 112 | totalTestCases = testResult.getTotalCount(); 113 | failedTestCases = testResult.getFailCount(); 114 | } 115 | 116 | String changes = getChangesText(build); 117 | 118 | int itemId = postBuild(apiFactory, build.getNumber(), result, url, 119 | changes, profiles, totalTestCases, failedTestCases, 120 | build.getDurationString()); 121 | 122 | AbstractBuild previousBuild = build.getPreviousBuild(); 123 | boolean oldFailed = previousBuild != null 124 | && previousBuild.getResult() != Result.SUCCESS; 125 | 126 | TaskAPI taskAPI = apiFactory.getAPI(TaskAPI.class); 127 | if (oldFailed && build.getResult() == Result.SUCCESS) { 128 | Run firstFailed = getFirstFailure(previousBuild); 129 | Integer firstFailedItemId = getItemId(apiFactory, 130 | firstFailed.getNumber()); 131 | if (firstFailedItemId != null) { 132 | List tasks = taskAPI.getTasksWithReference(new Reference( 133 | ReferenceType.ITEM, firstFailedItemId)); 134 | for (Task task : tasks) { 135 | if (task.getStatus() == TaskStatus.ACTIVE) { 136 | taskAPI.completeTask(task.getId()); 137 | } 138 | } 139 | } 140 | } else if (!oldFailed && build.getResult() != Result.SUCCESS) { 141 | String text = "Build " + build.getNumber() + " " 142 | + build.getResult().toString().toLowerCase() + ""; 143 | 144 | String description = null; 145 | if (testResult != null && testResult.getFailCount() > 0) { 146 | description += testResult.getFailCount() 147 | + " testcase(s) failed:\n"; 148 | 149 | List failedTests = testResult.getFailedTests(); 150 | for (CaseResult caseResult : failedTests) { 151 | description += caseResult.getDisplayName() + "\n"; 152 | } 153 | } 154 | 155 | for (ProfileMini profile : profiles) { 156 | taskAPI.createTaskWithReference( 157 | new TaskCreate(text, description, false, 158 | new LocalDate(), profile.getUserId()), 159 | new Reference(ReferenceType.ITEM, itemId), true); 160 | } 161 | } 162 | 163 | return true; 164 | } 165 | 166 | private Run getFirstFailure(Run build) { 167 | Run previousBuild = build.getPreviousBuild(); 168 | 169 | if (previousBuild != null) { 170 | if (previousBuild.getResult() == Result.SUCCESS) { 171 | return build; 172 | } 173 | 174 | return getFirstFailure(previousBuild); 175 | } else { 176 | return build; 177 | } 178 | } 179 | 180 | private Integer getItemId(APIFactory apiFactory, int buildNumber) { 181 | ItemsResponse response = apiFactory.getAPI(ItemAPI.class) 182 | .getItemsByExternalId(Integer.parseInt(appId), 183 | Integer.toString(buildNumber)); 184 | if (response.getFiltered() != 1) { 185 | return null; 186 | } 187 | 188 | return response.getItems().get(0).getId(); 189 | } 190 | 191 | private int postBuild(APIFactory apiFactory, int buildNumber, 192 | String result, String url, String changes, 193 | Set profiles, Integer totalTestCases, 194 | Integer failedTestCases, String duration) { 195 | List fields = new ArrayList(); 196 | fields.add(new FieldValuesUpdate("build-number", "value", "Build " 197 | + buildNumber)); 198 | fields.add(new FieldValuesUpdate("result", "value", result)); 199 | fields.add(new FieldValuesUpdate("url", "value", url)); 200 | if (changes != null) { 201 | fields.add(new FieldValuesUpdate("changes", "value", changes)); 202 | } 203 | List> subValues = new ArrayList>(); 204 | for (ProfileMini profile : profiles) { 205 | subValues.add(Collections. singletonMap("value", 206 | profile.getProfileId())); 207 | } 208 | fields.add(new FieldValuesUpdate("developers", subValues)); 209 | if (totalTestCases != null) { 210 | fields.add(new FieldValuesUpdate("total-testcases", "value", 211 | totalTestCases)); 212 | } 213 | if (failedTestCases != null) { 214 | fields.add(new FieldValuesUpdate("failed-testcases", "value", 215 | failedTestCases)); 216 | } 217 | fields.add(new FieldValuesUpdate("duration", "value", duration)); 218 | ItemCreate create = new ItemCreate(Integer.toString(buildNumber), 219 | fields, Collections. emptyList(), 220 | Collections. emptyList()); 221 | 222 | int itemId = apiFactory.getAPI(ItemAPI.class).addItem( 223 | Integer.parseInt(appId), create, true); 224 | 225 | return itemId; 226 | } 227 | 228 | @Override 229 | public boolean needsToRunAfterFinalized() { 230 | return true; 231 | } 232 | 233 | private int getSpace(APIFactory apiFactory) { 234 | return apiFactory.getAPI(AppAPI.class).getApp(Integer.parseInt(appId)) 235 | .getSpaceId(); 236 | } 237 | 238 | private Set getProfiles(APIFactory apiFactory, int spaceId, 239 | AbstractBuild build) { 240 | Set profiles = new HashSet(); 241 | 242 | Set culprits = build.getCulprits(); 243 | if (culprits.size() > 0) { 244 | for (User culprit : culprits) { 245 | ProfileMini profile = getProfile(apiFactory, spaceId, culprit); 246 | if (profile != null) { 247 | profiles.add(profile); 248 | } 249 | } 250 | } 251 | ChangeLogSet changeSet = build.getChangeSet(); 252 | if (changeSet != null) { 253 | for (Entry entry : changeSet) { 254 | ProfileMini profile = getProfile(apiFactory, spaceId, 255 | entry.getAuthor()); 256 | if (profile != null) { 257 | profiles.add(profile); 258 | } 259 | } 260 | } 261 | 262 | return profiles; 263 | } 264 | 265 | private String getChangesText(AbstractBuild build) { 266 | ChangeLogSet changeSet = build.getChangeSet(); 267 | if (changeSet == null || changeSet.isEmptySet()) { 268 | return null; 269 | } 270 | 271 | String out = ""; 272 | for (Entry entry : changeSet) { 273 | if (out.length() > 0) { 274 | out += "\n"; 275 | } 276 | 277 | out += entry.getMsgAnnotated(); 278 | } 279 | 280 | return out; 281 | } 282 | 283 | private ProfileMini getProfile(APIFactory apiFactory, int spaceId, User user) { 284 | UserProperty mailProperty = user.getProperty(Mailer.UserProperty.class); 285 | if (mailProperty == null) { 286 | return null; 287 | } 288 | String mail = mailProperty.getAddress(); 289 | if (mail == null) { 290 | return null; 291 | } 292 | 293 | List contacts = apiFactory.getAPI(ContactAPI.class) 294 | .getSpaceContacts(spaceId, ProfileField.MAIL, mail, 1, null, 295 | ProfileType.MINI, null); 296 | if (contacts.isEmpty()) { 297 | return null; 298 | } 299 | 300 | return contacts.get(0); 301 | } 302 | 303 | @Extension 304 | public static final class DescriptorImpl extends 305 | BuildStepDescriptor { 306 | 307 | private String username; 308 | 309 | private String password; 310 | 311 | private String clientId; 312 | 313 | private String clientSecret; 314 | 315 | public DescriptorImpl() { 316 | super(PodioBuildNotifier.class); 317 | load(); 318 | } 319 | 320 | @Override 321 | public String getDisplayName() { 322 | return "Podio Build Poster"; 323 | } 324 | 325 | @Override 326 | public boolean configure(StaplerRequest req, JSONObject formData) 327 | throws FormException { 328 | req.bindParameters(this); 329 | this.username = formData.getString("username"); 330 | this.password = formData.getString("password"); 331 | this.clientId = formData.getString("clientId"); 332 | this.clientSecret = formData.getString("clientSecret"); 333 | save(); 334 | return super.configure(req, formData); 335 | } 336 | 337 | public FormValidation doValidateAuth( 338 | @QueryParameter("appId") final String appId) 339 | throws IOException, ServletException { 340 | APIFactory apiFactory = new APIFactory(new ResourceFactory( 341 | new OAuthClientCredentials(clientId, clientSecret), 342 | new OAuthUsernameCredentials(username, password))); 343 | 344 | try { 345 | Application app = apiFactory.getAPI(AppAPI.class).getApp( 346 | Integer.parseInt(appId)); 347 | return FormValidation.ok("Connection ok, using app " 348 | + app.getConfiguration().getName()); 349 | } catch (UniformInterfaceException e) { 350 | if (e.getResponse().getStatus() == 404) { 351 | return FormValidation.error("No app found with the id " 352 | + appId); 353 | } else { 354 | return FormValidation.error("Invalid username or password"); 355 | } 356 | } catch (Exception e) { 357 | e.printStackTrace(); 358 | return FormValidation.error("Invalid username or password"); 359 | } 360 | } 361 | 362 | public FormValidation doValidateAPI( 363 | @QueryParameter("username") final String username, 364 | @QueryParameter("password") final String password, 365 | @QueryParameter("clientId") final String clientId, 366 | @QueryParameter("clientSecret") final String clientSecret) 367 | throws IOException, ServletException { 368 | APIFactory baseAPI = new APIFactory(new ResourceFactory( 369 | new OAuthClientCredentials(clientId, clientSecret), 370 | new OAuthUsernameCredentials(username, password))); 371 | 372 | try { 373 | String name = baseAPI.getAPI(UserAPI.class).getProfile() 374 | .getName(); 375 | return FormValidation.ok("Connection validated, logged in as " 376 | + name); 377 | } catch (Exception e) { 378 | e.printStackTrace(); 379 | return FormValidation.error("Invalid hostname, port or ssl"); 380 | } 381 | } 382 | 383 | @Override 384 | public Publisher newInstance(StaplerRequest req, JSONObject formData) 385 | throws FormException { 386 | return super.newInstance(req, formData); 387 | } 388 | 389 | @Override 390 | public boolean isApplicable(Class jobType) { 391 | return true; 392 | } 393 | 394 | public String getUsername() { 395 | return username; 396 | } 397 | 398 | public void setUsername(String username) { 399 | this.username = username; 400 | } 401 | 402 | public String getPassword() { 403 | return password; 404 | } 405 | 406 | public void setPassword(String password) { 407 | this.password = password; 408 | } 409 | 410 | public String getClientId() { 411 | return clientId; 412 | } 413 | 414 | public void setClientId(String clientId) { 415 | this.clientId = clientId; 416 | } 417 | 418 | public String getClientSecret() { 419 | return clientSecret; 420 | } 421 | 422 | public void setClientSecret(String clientSecret) { 423 | this.clientSecret = clientSecret; 424 | } 425 | } 426 | } 427 | --------------------------------------------------------------------------------