Skip to content

Commit

Permalink
Delayed approval mechanism [FIXED JENKINS-11409]
Browse files Browse the repository at this point in the history
This patch introduces the possibility for a delayed approval to be sent
back to Gerrit. This is a relatively advanced use case, so the setting
has been placed in the Advanced section for the Trigger settings.
When one selects "delayed approval", the completion of the build will
NOT send an approval back to Gerrit. Instead, the triggering event stays
open, and needs to be closed later on. This can be done by using the new
post-build action "Send a Gerrit delayed approval". At the moment, this
notifier expects to read the name of the job and build number from build
variables. If these point to an existing build which has a Gerrit event
as a trigger, which has not been submitted due to "delayed approval",
then that approval will be sent. In particular, this allows to modify
the status of that build through other job (e.g., using Groovy scripts),
and mark it UNSTABLE (e.g., if a subsequent build were to fail), which
is a way one can change the result of the Approval in Gerrit.

Amends: fixed broken style and broken unit tests
  • Loading branch information
yannack committed Jan 6, 2014
1 parent b2028be commit 5f672f2
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 18 deletions.
Expand Up @@ -93,9 +93,10 @@ public synchronized void onCompleted(AbstractBuild r, TaskListener listener) {
GerritCause cause = getCause(r);
logger.info("Completed. Build: {} Cause: {}", r, cause);
if (cause != null) {
GerritTrigger trigger = GerritTrigger.getTrigger(r.getProject());
cleanUpGerritCauses(cause, r);
GerritTriggeredEvent event = cause.getEvent();
if (GerritTrigger.getTrigger(r.getProject()) != null) {
if (trigger != null) {
// There won't be a trigger if this job was run through a unit test
GerritTrigger.getTrigger(r.getProject()).notifyBuildEnded(event);
}
Expand Down Expand Up @@ -123,24 +124,43 @@ public synchronized void onCompleted(AbstractBuild r, TaskListener listener) {
}

updateTriggerContexts(r);
if (memory.isAllBuildsCompleted(event)) {
try {
logger.info("All Builds are completed for cause: {}", cause);
if (event instanceof GerritEventLifecycle) {
((GerritEventLifecycle)event).fireAllBuildsCompleted();
}
NotificationFactory.getInstance().queueBuildCompleted(memory.getMemoryImprint(event), listener);
} finally {
memory.forget(event);
if (trigger != null) {
if (trigger.isDelayedApproval()) {
logger.info("Delayed approval set. Waiting for delayed approval for cause [{}]. Status: \n{}",
cause, memory.getStatusReport(event));
return;
}
} else {
logger.info("Waiting for more builds to complete for cause [{}]. Status: \n{}",
cause, memory.getStatusReport(event));
}
allBuildsCompleted(event, cause, listener);
}
}
}

/**
* Manages the end of a Gerrit Event. Should be called after each build related to an event completes if that build
* should report back to Gerrit.
*
* @param event the Gerrit Event which may need to be completed.
* @param cause the Gerrit Cause which triggered the build initially.
* @param listener the Jenkins listener.
*/
public synchronized void allBuildsCompleted(GerritTriggeredEvent event, GerritCause cause, TaskListener listener) {
if (memory.isAllBuildsCompleted(event)) {
try {
logger.info("All Builds are completed for cause: {}", cause);
if (event instanceof GerritEventLifecycle) {
((GerritEventLifecycle)event).fireAllBuildsCompleted();
}
NotificationFactory.getInstance().queueBuildCompleted(memory.getMemoryImprint(event), listener);
} finally {
memory.forget(event);
}
} else {
logger.info("Waiting for more builds to complete for cause [{}]. Status: \n{}",
cause, memory.getStatusReport(event));
}
}

@Override
public synchronized void onStarted(AbstractBuild r, TaskListener listener) {
GerritCause cause = getCause(r);
Expand Down
@@ -0,0 +1,212 @@
/*
* The MIT License
*
* Copyright 2010 Sony Ericsson Mobile Communications. All rights reserved.
* Copyright 2012 Sony Mobile Communications AB. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.sonyericsson.hudson.plugins.gerrit.trigger.hudsontrigger;


import hudson.Extension;
import hudson.Util;
//import org.slf4j.Logger;
//import org.slf4j.LoggerFactory;
import org.kohsuke.stapler.DataBoundConstructor;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Notifier;
import hudson.tasks.Publisher;
import hudson.model.AbstractProject;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.Hudson;
import java.util.Map;
import hudson.Launcher;
import com.sonyericsson.hudson.plugins.gerrit.trigger.gerritnotifier.ToGerritRunListener;


/**
* Triggers a build previously run to send its report to Gerrit, if it hadn't yet.
*
* @author Yannick Bréhon <yannick.brehon@smartmatic.com>
*/

public class GerritDelayedApprover extends Notifier {

//private static final Logger logger = LoggerFactory.getLogger(GerritDelayedApprover.class);
private String delayedJob;
private String delayedBuildNumber;


/**
* Default DataBound Constructor.
*
* @param delayedJob the name of the job which may need a delayed approval. buildVariables are expanded.
* @param delayedBuildNumber the number of the build which may need a delayed approval. buildVariables are expanded.
*/
@DataBoundConstructor
public GerritDelayedApprover(
String delayedJob,
String delayedBuildNumber) {
this.delayedJob = delayedJob;
this.delayedBuildNumber = delayedBuildNumber;
}
/**
* The delayedBuildNumber, ie number of the build which may need a delayed approval.
*
* @return the delayedBuildNumber
*/
public String getDelayedBuildNumber() {
return delayedBuildNumber;
}

/**
* Sets the delayedBuildNumber.
*
* @param delayedBuildNumber
* the delayedBuildNumber
*/
public void setDelayedBuildNumber(String delayedBuildNumber) {
this.delayedBuildNumber = delayedBuildNumber;
}

/**
* The delayedJob, ie the name of the job which may need a delayed approval.
*
* @return the delayedJob
*/
public String getDelayedJob() {
return delayedJob;
}

/**
* Sets the delayedJob.
*
* @param delayedJob
* the delayedJob
*/
public void setDelayedJob(String delayedJob) {
this.delayedJob = delayedJob;
}

/**
* No concurrency management is required, new style extension.
* @return BuildStepMonitor.NONE
*
*/
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
}

/**
* This extension needs to run after the build is completed.
* @return true
*
*/
@Override
public boolean needsToRunAfterFinalized() {
return true;
}

/**
* The actual performer method which will run in a post-build step and close a Gerrit Triggered event.
* @param build current build
* @param launcher launcher
* @param listener the build listener
* @throws InterruptedException if interrupted
* @return boolean indicating whether this perform step succeeded
*/
public boolean perform(final AbstractBuild<?, ?> build, final Launcher launcher, final BuildListener listener)
throws InterruptedException {
//logger.info("Running Gerrit Delayed Approval");
Map<String, String> buildVars = build.getBuildVariables();
String jobName = Util.replaceMacro(delayedJob, buildVars);
String numberStr = Util.replaceMacro(delayedBuildNumber, buildVars);
int number = Integer.parseInt(numberStr);

//String jobName = (String) build.getBuildVariables().get(jobParameterName);
//int number = Integer.parseInt((String)build.getBuildVariables().get(buildNumParameterName));
AbstractProject initiatingJob = Hudson.getInstance().getItemByFullName(jobName, AbstractProject.class);
if (initiatingJob == null) {
return false;
}
AbstractBuild initiatingBuild = (AbstractBuild)initiatingJob.getBuildByNumber(number);
GerritCause cause = (GerritCause)initiatingBuild.getCause(GerritCause.class);
if (cause == null) {
return false;
}
ToGerritRunListener thelistener = ToGerritRunListener.getInstance();
thelistener.allBuildsCompleted(cause.getEvent(), cause, null);
return true;
}

/*---------------------------------*/
/* ----- Descriptor stuff -------- */
/*---------------------------------*/

/**
* getter for the Descriptor.
* @return the associated descriptor
*/
@Override
public BuildStepDescriptor<Publisher> getDescriptor() {
return DESCRIPTOR;
}

/**
* The associated descriptor instance.
*/
public static final GerritDelayedApproverDescriptor DESCRIPTOR = new GerritDelayedApproverDescriptor();

/**
* Descriptor class.
*/
@Extension
public static final class GerritDelayedApproverDescriptor extends
BuildStepDescriptor<Publisher> {

//private static final Logger logger = LoggerFactory.getLogger(GerritDelayedApproverDescriptor.class);

/**
* Overridden constructor, needed to reload saved data.
*/
public GerritDelayedApproverDescriptor() {
super(GerritDelayedApprover.class);
load();
}

@Override
public String getDisplayName() {
return "Send a delayed Gerrit approval";
}

@Override
public boolean isApplicable(Class<? extends AbstractProject> jobType) {
return true;
}

/*@Override
public String getHelpFile() {
return "/plugin/build-publisher/help/config/publish.html";
}*/

}
}
Expand Up @@ -123,6 +123,7 @@ public class GerritTrigger extends Trigger<AbstractProject> implements GerritEve
private Integer gerritBuildNotBuiltVerifiedValue;
private Integer gerritBuildNotBuiltCodeReviewValue;
private boolean silentMode;
private boolean delayedApproval;
private boolean escapeQuotes;
private boolean noNameAndEmailParameters;
private String buildStartMessage;
Expand Down Expand Up @@ -178,6 +179,7 @@ public class GerritTrigger extends Trigger<AbstractProject> implements GerritEve
* Job specific Gerrit code review vote when a build is not built, null means
* that the global value should be used.
* @param silentMode Silent Mode on or off.
* @param delayedApproval Delayed Approval on or off.
* @param escapeQuotes EscapeQuotes on or off.
* @param noNameAndEmailParameters Whether to create parameters containing name and email
* @param buildStartMessage Message to write to Gerrit when a build begins
Expand Down Expand Up @@ -210,6 +212,7 @@ public GerritTrigger(
Integer gerritBuildNotBuiltVerifiedValue,
Integer gerritBuildNotBuiltCodeReviewValue,
boolean silentMode,
boolean delayedApproval,
boolean escapeQuotes,
boolean noNameAndEmailParameters,
String buildStartMessage,
Expand Down Expand Up @@ -237,6 +240,7 @@ public GerritTrigger(
this.gerritBuildNotBuiltVerifiedValue = gerritBuildNotBuiltVerifiedValue;
this.gerritBuildNotBuiltCodeReviewValue = gerritBuildNotBuiltCodeReviewValue;
this.silentMode = silentMode;
this.delayedApproval = delayedApproval;
this.escapeQuotes = escapeQuotes;
this.noNameAndEmailParameters = noNameAndEmailParameters;
this.buildStartMessage = buildStartMessage;
Expand Down Expand Up @@ -1234,6 +1238,16 @@ public boolean isSilentMode() {
return silentMode;
}

/**
* If delayed approval is on or off. When delayed approval is on there will be no automatic result of the build
* sent back to Gerrit. This will have to be sent using a different mechanism. Default is false.
*
* @return true if delayed approval is on.
*/
public boolean isDelayedApproval() {
return delayedApproval;
}

/**
* if escapeQuotes is on or off. When escapeQuotes is on this plugin will escape quotes in Gerrit event parameter
* string Default is true
Expand Down Expand Up @@ -1340,6 +1354,16 @@ public void setSilentMode(boolean silentMode) {
this.silentMode = silentMode;
}

/**
* Sets delayed approval to on or off. When delayed approval is on there will be no automatic result of the
* build sent back to Gerrit. This will have to be sent using a different mechanism. Default is false.
*
* @param delayedApproval true if delayed approval should be on.
*/
public void setDelayedApproval(boolean delayedApproval) {
this.delayedApproval = delayedApproval;
}

/**
* URL to send in comment to Gerrit.
*
Expand Down
@@ -0,0 +1,8 @@
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:entry name="delayedJob" title="Name of the job name which may need a delayed approval" field="delayedJob">
<f:textbox name="delayedJob" value="${instance.delayedJob==null?'$JOB_TO_GERRITCOMPLETE':instance.delayedJob}"/>
</f:entry>
<f:entry name="delayedBuildNumber" title="Build number which may need a delayed approval" field="delayedBuildNumber">
<f:textbox name="delayedBuildNumber" value="${instance.delayedBuildNumber==null?'$BUILD_TO_GERRITCOMPLETE':instance.delayedBuildNumber}"/>
</f:entry>
</j:jelly>
Expand Up @@ -15,6 +15,10 @@
help="/plugin/gerrit-trigger/trigger/help-NoNameAndEmailParameters.html">
<f:checkbox name="noNameAndEmailParameters" default="false" checked="${it.noNameAndEmailParameters}"/>
</f:entry>
<f:entry title="${%Delayed Approval}" field="delayedApproval"
help="/plugin/gerrit-trigger/trigger/help-DelayedApproval.html">
<f:checkbox name="delayedApproval" default="false" checked="${it.delayedApproval}"/>
</f:entry>
<f:section title="${%Gerrit Reporting Values}">
<f:entry title="${%URL to post}"
field="customUrl"
Expand Down
@@ -0,0 +1 @@
Select this option to prevent Jenkins from sending the approval at the end of this build. A post-build "delayed approval" will be necessary to have the result sent back to Gerrit.
Expand Up @@ -263,7 +263,7 @@ public void testInitializeTriggerOnEvents() {
AbstractProject project = PowerMockito.mock(AbstractProject.class);
when(project.getFullName()).thenReturn("MockedProject");
GerritTrigger trigger = new GerritTrigger(null, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
true, true, false, "", "", "", "", "", "", null, null, null, false, false, "");
true, false, true, false, "", "", "", "", "", "", null, null, null, false, false, "");
trigger = spy(trigger);
Object triggerOnEvents = Whitebox.getInternalState(trigger, "triggerOnEvents");
assertNull(triggerOnEvents);
Expand Down

0 comments on commit 5f672f2

Please sign in to comment.