package hudson.plugins.collabnet.tracker; import com.collabnet.ce.webservices.CTFArtifact; import com.collabnet.ce.webservices.CTFFile; import com.collabnet.ce.webservices.CTFProject; import com.collabnet.ce.webservices.CTFRelease; import com.collabnet.ce.webservices.CTFTracker; import com.collabnet.ce.webservices.CollabNetApp; import hudson.Extension; import hudson.Launcher; import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.BuildListener; import hudson.model.Hudson; import hudson.model.Result; import hudson.model.TaskListener; import hudson.plugins.collabnet.AbstractTeamForgeNotifier; import hudson.plugins.collabnet.ConnectionFactory; import hudson.plugins.collabnet.util.CNFormFieldValidator; import hudson.plugins.collabnet.util.CNHudsonUtil; import hudson.plugins.collabnet.util.ComboBoxUpdater; import hudson.plugins.collabnet.util.CommonUtil; import hudson.tasks.BuildStepMonitor; import hudson.util.ComboBoxModel; import hudson.util.FormValidation; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import java.io.IOException; import java.rmi.RemoteException; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.logging.Logger; import static hudson.model.Result.SUCCESS; import static hudson.model.Result.UNSTABLE; public class CNTracker extends AbstractTeamForgeNotifier { private static int DEFAULT_PRIORITY = Priority.DEFAULT.n; // listener is used for logging and will only be // set at the beginning of perform. private transient BuildListener listener = null; // data from jelly private String tracker = null; private String title = null; private String assign_user = null; private int priority = DEFAULT_PRIORITY; // for compatibility reason this has to be persisted as an integer private boolean attach_log = true; private boolean always_update = false; private boolean close_issue = true; private String release; // collabNet object private transient CollabNetApp cna = null; /** * Constructs a new CNTracker instance. * * @param tracker tracker name. * @param title title to use when create new tracker artifacts OR to find * existing tracker artifacts. * @param assignUser user to assign new tracker artifacts to. * @param priority of new tracker artifacts. * @param attachLog if true, Hudson build logs will be uploaded and * attached when creating/updating tracker artifacts. * @param alwaysUpdate if true, always update the tracker artifacts (or * create one), even if build is successful and * the tracker artifact is closed. If false, only * update when the tracker artifact is failing * or is open. * @param closeOnSuccess if true, the tracker artifact will be closed if the * Hudson build is successful. Otherwise, open issues * will be updated with a successful message, but * remain open. * @param release to report the tracker artifact in. */ @DataBoundConstructor public CNTracker(ConnectionFactory connectionFactory, String project, String tracker, String title, String assignUser, Priority priority, boolean attachLog, boolean alwaysUpdate, boolean closeOnSuccess, String release) { super(connectionFactory,project); this.tracker = tracker; this.title = title; this.assign_user = assignUser; this.priority = priority.n; this.attach_log = attachLog; this.always_update = alwaysUpdate; this.close_issue = closeOnSuccess; this.release = release; } /** * Setting the listener allows logging to work. * * @param listener handles build events. */ private void setupLogging(BuildListener listener) { this.listener = listener; } /** * Logging will only work once the listener is set. * Otherwise, it will fail (silently). * * @param message to print to the console. */ private void log(String message) { if (this.listener != null) { message = "CollabNet Tracker: " + message; this.listener.getLogger().println(message); } } /** * Convenience method to log RemoteExceptions. * * @param methodName in progress when the exception occurred. * @param re RemoteException that occurred. */ private void log(String methodName, RemoteException re) { this.log(methodName + " failed due to " + re.getClass().getName() + ": " + re.getMessage()); } /** * @return tracker name. */ public String getTracker() { return this.tracker; } /** * @return title for the Tracker Artifact. */ public String getTitle() { return this.title; } /** * @return the user to assign new Tracker Artifacts to. */ public String getAssignUser() { return Util.fixEmpty(this.assign_user); } /** * @return the priority to set new Tracker Artifacts to. */ public Priority getPriority() { return Priority.valueOf(this.priority); } /** * @return true, if logs should be attached to Tracker Artifacts. */ public boolean getAttachLog() { return this.attach_log; } /** * @return true, if artifact creation/update should happen, even if * the Hudson build is successful and the artifact is not open. */ public boolean getAlwaysUpdate() { return this.always_update; } /** * @return true, if artifacts should be closed when the Hudson build * succeeds. */ public boolean getCloseOnSuccess() { return this.close_issue; } /** * @return the name of the release which new Tracker Artifacts will be * reported in. */ public String getRelease() { return this.release; } @Override public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.BUILD; } /** * Create/Update/Close the tracker issue, according to the Hudson * build status. * * @param build the current Hudson build. * @param launcher unused. * @param listener receives events that occur during a build; used for * logging. * @return false if a critical error occurred, true otherwise. */ @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { this.setupLogging(listener); this.cna = connect(); if (this.cna == null) { this.log("Critical Error: login to " + this.getCollabNetUrl() + " failed. Setting build status to UNSTABLE (or worse)."); build.setResult(UNSTABLE); return false; } try { CTFProject p = cna.getProjectByTitle(getProject()); if (p == null) { this.log("Critical Error: projectId cannot be found for " + this.getProject() + ". This could mean that the project " + "does not exist OR that the user logging in does not " + "have access to that project. " + "Setting build status to UNSTABLE (or worse)."); build.setResult(UNSTABLE); return false; } CTFTracker t = p.getTrackers().byTitle(this.tracker); if (t == null) { this.log("Critical Error: trackerId cannot be found for " + this.tracker + ". This could mean the tracker does " + "not exist OR that the user logging in does not have " + "access to that tracker. " + "Setting build status to UNSTABLE (or worse)."); build.setResult(UNSTABLE); return false; } CTFArtifact issue = this.findTrackerArtifact(t, build); Result buildStatus = build.getResult(); if (issue == null) { // no issue and failure found if (buildStatus.isWorseThan(SUCCESS)) { this.log("Build is not successful; opening a new issue."); String description = "The build has failed. Latest " + "build status: " + build.getResult() + ". For more info, " + "see " + this.getBuildUrl(build); this.createNewTrackerArtifact(t,"Open", description, build); // no issue and success may open a new issue if we're updating // no matter what } else { this.log("Build is successful!"); if (this.getAlwaysUpdate()) { String description = "The build has succeeded. For " + "more info, see " + this.getBuildUrl(build); this.createNewTrackerArtifact(t,"Closed", description, build); } } } // update existing fail -> fail else if (issue.getStatusClass().equals("Open") && buildStatus.isWorseThan(SUCCESS)) { this.log("Build is continuing to fail; updating issue."); this.updateFailingBuild(issue, build); } // close or update existing fail -> succeed else if (issue.getStatusClass().equals("Open") && buildStatus.isBetterOrEqualTo(SUCCESS)) { if (this.getCloseOnSuccess()) { this.log("Build succeeded; closing issue."); this.closeSucceedingBuild(issue, build); } else { // just update this.log("Build succeeded; updating issue."); this.updateSucceedingBuild(issue, build); } } // create new succeed -> fail else if (issue.getStatusClass().equals("Close") && buildStatus.isWorseThan(SUCCESS)) { // create new or reopen? if (this.getAlwaysUpdate()) { this.log("Build is not successful; re-opening issue."); this.updateFailingBuild(issue, build); } else { this.log("Build is not successful; opening a new issue."); String description = "The build has failed. Latest " + "build status: " + build.getResult() + ". For more " + "info, see " + this.getBuildUrl(build); this.createNewTrackerArtifact(t, "Open", description, build); } } else if (issue.getStatusClass().equals("Close") && buildStatus.isBetterOrEqualTo(SUCCESS)) { this.log("Build continues to be successful!"); if (this.getAlwaysUpdate()) { this.updateSucceedingBuild(issue, build); } } else { this.log("Unexpected state: result is: " + buildStatus + ". Issue status " + "class is: " + issue.getStatusClass() + "."); } return true; } finally { logoff(); } } /** * Log out of the collabnet server. */ public void logoff() { CNHudsonUtil.logoff(this.cna); this.cna = null; } /** * Return a tracker artifact with the matching title. * * @param build the current Hudson build. * @return the artifact soap data object, if one exists which matches * the title. Otherwise, null. */ public CTFArtifact findTrackerArtifact(CTFTracker tracker, AbstractBuild<?, ?> build) throws IOException, InterruptedException { if (this.cna == null) { this.log("Cannot call findTrackerArtifact, not logged in!"); return null; } String title = this.getInterpreted(build, this.getTitle()); List<CTFArtifact> r = tracker.getArtifactsByTitle(title); Collections.sort(r, new Comparator<CTFArtifact>() { public int compare(CTFArtifact o1, CTFArtifact o2) { return o2.getLastModifiedDate().compareTo(o1.getLastModifiedDate()); } }); if (r.size()>0) return r.get(0); return null; } /** * Create a new tracker artifact with the given values. * * @param status status to set on the new artifact (Open, Closed, etc.). * @param description description of the new artifact. * @return the newly created ArtifactSoapDO. */ public CTFArtifact createNewTrackerArtifact(CTFTracker t, String status, String description, AbstractBuild <?, ?> build) throws IOException, InterruptedException { if (this.cna == null) { this.log("Cannot call createNewTrackerArtifact, not logged in!"); return null; } CTFFile buildLog = null; if (this.getAttachLog()) { buildLog = this.uploadBuildLog(build); if (buildLog != null) { this.log("Successfully uploaded build log."); } else { this.log("Failed to upload build log."); } } // check assign user validity String assignTo = this.getValidAssignUser(t.getProject()); String title = this.getInterpreted(build, this.getTitle()); CTFRelease release = CNHudsonUtil.getProjectReleaseId(t.getProject(),this.getRelease()); try { CTFArtifact asd = t.createArtifact(title, description, null, null, status, null, this.priority, 0, assignTo, release!=null?release.getId():null, null, build.getLogFile().getName(), "text/plain", buildLog); this.log("Created tracker artifact '" + title + "' in tracker '" + this.getTracker() + "' in project '" + this.getProject() + "' on behalf of '" + this.getUsername() + "' at " + asd.getURL() + "."); return asd; } catch (RemoteException re) { this.log("createNewTrackerArtifact", re); return null; } } /** * @return the assigned user, if that user is a member of the project. * Otherwise, null. */ private String getValidAssignUser(CTFProject p) throws RemoteException { if (!p.hasMember(this.assign_user)) { this.log("User (" + this.assign_user + ") is not a member of " + "the project (" + this.getProject() + "). " + "Instead " + "any new issues filed will be assigned to 'None'."); return null; } return this.assign_user; } /** * Update the issue with failing build status. * * @param issue the existing issue. * @param build the current Hudson build. */ public void updateFailingBuild(CTFArtifact issue, AbstractBuild<?, ?> build) throws IOException, InterruptedException { if (this.cna == null) { this.log("Cannot call updateFailingBuild, not logged in!"); return; } CTFFile buildLog = null; if (this.getAttachLog()) { buildLog = this.uploadBuildLog(build); if (buildLog != null) { this.log("Successfully uploaded build log."); } else { this.log("Failed to upload build log."); } } String update = "Updated"; if (!issue.getStatus().equals("Open")) { issue.setStatus("Open"); update = "Updated and reopened"; } String comment = "The build is continuing to fail. Latest " + "build status: " + build.getResult() + ". For more info, see " + this.getBuildUrl(build); String title = this.getInterpreted(build, this.getTitle()); try { issue.update(comment, build.getLogFile().getName(), "text/plain", buildLog); this.log(update + " tracker artifact '" + title + "' in tracker '" + this.getTracker() + "' in project '" + this.getProject() + "' on behalf of '" + this.getUsername() + "' at " + issue.getURL() + " with failed status."); } catch (RemoteException re) { this.log("updateFailingBuild", re); } catch (IOException ioe) { this.log("updateFailingBuild failed due to IOException:" + ioe.getMessage()); } } /** * Update the issue with a build that's successful, but do not change * its status. * * @param issue the existing issue. * @param build the current Hudson build. */ public void updateSucceedingBuild(CTFArtifact issue, AbstractBuild<?, ?> build) throws IOException, InterruptedException { if (this.cna == null) { this.log("Cannot call updateSucceedingBuild, not logged in!"); return; } CTFFile buildLog = null; if (this.getAttachLog()) { buildLog = this.uploadBuildLog(build); if (buildLog != null) { this.log("Successfully uploaded build log."); } else { this.log("Failed to upload build log."); } } String comment = "The build is succeeding. For more info, " + "see " + this.getBuildUrl(build); String title = this.getInterpreted(build, this.getTitle()); try { issue.update(comment, build.getLogFile().getName(), "text/plain", buildLog); this.log("Updated tracker artifact '" + title + "' in tracker '" + this.getTracker() + "' in project '" + this.getProject() + "' on behalf of '" + this.getUsername() + "' at " + issue.getURL() + " with successful status."); } catch (RemoteException re) { this.log("updateSucceedingBuild", re); } catch (IOException ioe) { this.log("updateSuccedingBuild failed due to IOException:" + ioe.getMessage()); } } /** * Update the issue with a build that's successful, and close it. * * @param issue the existing issue. * @param build the current Hudson build. */ public void closeSucceedingBuild(CTFArtifact issue, AbstractBuild<?, ?> build) throws IOException, InterruptedException { if (this.cna == null) { this.log("Cannot call updateSucceedingBuild, not logged in!"); return; } CTFFile buildLog = null; if (this.getAttachLog()) { buildLog = this.uploadBuildLog(build); if (buildLog != null) { this.log("Successfully uploaded build log."); } else { this.log("Failed to upload build log."); } } String comment = "The build succeeded! Closing issue. " + "For more info, see " + this.getBuildUrl(build); issue.setStatusClass("Close"); issue.setStatus("Closed"); String title = this.getInterpreted(build, this.getTitle()); try { issue.update(comment, build.getLogFile().getName(), "text/plain", buildLog); this.log("Closed tracker artifact '" + title + "' in tracker '" + this.getTracker() + "' in project '" + this.getProject() + "' on behalf of '" + this.getUsername() + "' at " + issue.getURL() + " with successful status."); } catch (RemoteException re) { this.log("closeSucceedingBuild", re); } } /** * Returns the absolute URL to the build, if rootUrl has been configured. * If not, returns the build number. * * @param build the current Hudson build. * @return the absolute URL for this build, or the a string containing the * build number. */ private String getBuildUrl(AbstractBuild<?, ?> build) { Hudson hudson = Hudson.getInstance(); String rootUrl = hudson.getRootUrl(); if (rootUrl == null) { return "Hudson Build #" + build.number; } else { return hudson.getRootUrl() + build.getUrl(); } } /** * Upload the build log to the collabnet server. * * @param build the current Hudson build. * @return the id associated with the file upload. */ private CTFFile uploadBuildLog(AbstractBuild <?, ?> build) { if (this.cna == null) { this.log("Cannot call updateSucceedingBuild, not logged in!"); return null; } try { return cna.upload(build.getLogFile()); } catch (RemoteException re) { this.log("uploadBuildLog", re); } return null; } /** * Translates a string that may contain build vars like ${BUILD_VAR} to * a string with those vars interpreted. * * @param build the Hudson build. * @param str the string to be interpreted. * @return the interpreted string. * @throws IllegalArgumentException if the env var is not found. */ private String getInterpreted(AbstractBuild<?, ?> build, String str) throws IOException, InterruptedException { try { return CommonUtil.getInterpreted(build.getEnvironment(TaskListener.NULL), str); } catch (IllegalArgumentException iae) { this.log(iae.getMessage()); throw iae; } } @Extension public static final class DescriptorImpl extends AbstractTeamForgeNotifier.DescriptorImpl { private static Logger log = Logger.getLogger("CNTrackerDescriptor"); /** * @return human readable name used in the configuration screen. */ @Override public String getDisplayName() { return "CollabNet Tracker"; } /** * Form validation for the tracker field. * * @param req StaplerRequest which contains parameters from the config.jelly. */ public FormValidation doCheckTracker(StaplerRequest req) throws RemoteException { return CNFormFieldValidator.trackerCheck(req); } /** * Form validation for "assign issue to". * * @param req StaplerRequest which contains parameters from the config.jelly. */ public FormValidation doCheckAssign(StaplerRequest req) throws RemoteException { return CNFormFieldValidator.assignCheck(req); } /** * Form validation for the comment and description. * * @param value * @param name of field */ public FormValidation doRequiredInterpretedCheck( @QueryParameter String value, @QueryParameter String name) throws FormValidation { return CNFormFieldValidator.requiredInterpretedCheck(value, name); } /** * Form validation for the release field. */ public FormValidation doCheckRelease(CollabNetApp cna, @QueryParameter String project, @QueryParameter("package") String rpackage, @QueryParameter String release) throws RemoteException { return CNFormFieldValidator.releaseCheck(cna,project,rpackage,release,false); } /********************************************** * Methods for updating editable combo boxes. * **********************************************/ /** * Gets a list of trackers to choose from and write them as a * JSON string into the response data. */ public ComboBoxModel doFillTrackerItems(CollabNetApp cna, @QueryParameter String project) throws RemoteException { return ComboBoxUpdater.getTrackerList(cna.getProjectByTitle(project)); } /** * Gets a list of projectUsers to choose from and write them as a * JSON string into the response data. */ public ComboBoxModel doFillAssignUserItems(CollabNetApp cna, @QueryParameter String project) throws IOException { return ComboBoxUpdater.getUsers(cna,project); } /** * Gets a list of releases to choose from and write them as a * JSON string into the response data. */ public ComboBoxModel doFillReleaseItems(CollabNetApp cna, @QueryParameter String project, @QueryParameter("package") String _package) throws RemoteException { return ComboBoxUpdater.getReleases(cna,project,_package); } } }