Reputation: 602
In our current project setup we have the Jobs A, B, C.
When A, B or C is built successfully, we want to deploy the resulting artifacts to our development server. Therefore we use job D.
Because the deployment recreates the development database, we only want to run the job at every full hour. Our testers are familiar with this schedule and work this way.
Actually when D gets triggered by A, B, or C, a script is started that waits until the server minute is 00. If one of the trigger jobs triggers D again while its waiting, the formerly waiting script gets canceled and restarted. If the script reaches 00 of the hour, the deployment happens.
The main problem is, that the job D blocks a building slot for 59 minutes in the worst case.
Running the job hourly by default is not an option, because then the deployment would happen, even if nothing changed.
Running the job after something changed is also bad, because the testers are used to the hourly deployment.
I know there is a 'quiet time' option, but that only enables me to set a waiting time relative to the trigger time. What I need is a 'quiet time' that delays a job until a certain time has reached. Has anybody an advice on how to reach this goal?
Upvotes: 4
Views: 3541
Reputation: 14762
I developed the following solution:
1) Use the Conditional BuildStep Plugin in jobs A, B and C to set the cron schedule in job D's config.xml
:
#!/bin/bash
echo " This script is a placeholder to represent a build step that can succeed or not succeed"
true # to test build success → Groovy script should be executed
#false # to test not successful build → nothing should be done
// From: How to delay Jenkins job until a certain time was reached?
// http://stackoverflow.com/questions/27952216/1744774
// -----------------------------------------------------------
// Adapt these according to your environment
final String DOWNSTREAM_NAME = 'SO-27952216-Downstream-job'
final String CRON_SCHEDULE = '0 * * * *'
// -----------------------------------------------------------
final String SEPARATOR = new String(new char[8]).replace('\0', '-')
println(" ${SEPARATOR} Adapting configuration of ${DOWNSTREAM_NAME} ${SEPARATOR}")
import jenkins.model.*
import hudson.model.*
final Project DOWNSTREAM_JOB = Jenkins.instance.getItem(DOWNSTREAM_NAME)
final String DOWNSTREAM_CONFIG = DOWNSTREAM_JOB.getRootDir().getPath() + "/config.xml"
//import hudson.triggers.*
//DOWNSTREAM_JOB.getTrigger(TimerTrigger.class).spec = CRON_SCHEDULE
// leads to:
// ERROR: Build step failed with exception groovy.lang.ReadOnlyPropertyException:
// Cannot set readonly property: spec for class: hudson.triggers.TimerTrigger
import org.w3c.dom.*
import javax.xml.parsers.*
import javax.xml.xpath.*
println(" Reading ${DOWNSTREAM_CONFIG}")
Document doc =
DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(DOWNSTREAM_CONFIG)
XPathExpression expr = XPathFactory.newInstance().newXPath()
.compile("/project/triggers/hudson.triggers.TimerTrigger/spec");
final org.w3c.dom.Node SCHEDULE_NODE = expr.evaluate(doc, XPathConstants.NODE)
println(String.format(
" Changing Build Triggers → Build periodically → Schedule from '%s' to ' %s'",
SCHEDULE_NODE.getTextContent(), CRON_SCHEDULE))
SCHEDULE_NODE.setTextContent(CRON_SCHEDULE)
import javax.xml.transform.*
import javax.xml.transform.dom.*
import javax.xml.transform.stream.*
println(" Writing ${DOWNSTREAM_CONFIG}")
Transformer transformer = TransformerFactory.newInstance().newTransformer()
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
transformer.transform(new DOMSource(doc), new StreamResult(new File(DOWNSTREAM_CONFIG)))
println(" ${SEPARATOR} Adapted configuration of ${DOWNSTREAM_NAME} ${SEPARATOR}")
2) Use Post-build Action → Groovy Postbuild in job D to reset the cron schedule in its config.xml
:
#!/bin/bash
echo " This script is a placeholder to represent a build step"
// From: How to delay Jenkins job until a certain time was reached?
// http://stackoverflow.com/questions/27952216/1744774
// -----------------------------------------------------------
// Adapt these according to your environment
final String THIS_JOB_NAME = 'SO-27952216-Downstream-job'
final String CRON_SCHEDULE = ''
// -----------------------------------------------------------
final Object LOG = manager.listener.logger
final String SEPARATOR = new String(new char[8]).replace('\0', '-')
LOG.println(" ${SEPARATOR} Adapting configuration of ${THIS_JOB_NAME} ${SEPARATOR}")
import jenkins.model.*
import hudson.model.*
final Project THIS_JOB = Jenkins.instance.getItem(THIS_JOB_NAME)
final String THIS_JOB_CONFIG = THIS_JOB.getRootDir().getPath() + "/config.xml"
//import hudson.triggers.*
//THIS_JOB.getTrigger(TimerTrigger.class).spec = CRON_SCHEDULE
// leads to:
// ERROR: Build step failed with exception groovy.lang.ReadOnlyPropertyException:
// Cannot set readonly property: spec for class: hudson.triggers.TimerTrigger
import org.w3c.dom.*;
import javax.xml.parsers.*
import javax.xml.xpath.*
LOG.println(" Reading ${THIS_JOB_CONFIG}")
Document doc =
DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(THIS_JOB_CONFIG)
XPathExpression expr = XPathFactory.newInstance().newXPath()
.compile("/project/triggers/hudson.triggers.TimerTrigger/spec")
final org.w3c.dom.Node SCHEDULE_NODE = expr.evaluate(doc, XPathConstants.NODE)
LOG.println(String.format(
" Changing Build Triggers → Build periodically → Schedule from '%s' to ' %s'",
SCHEDULE_NODE.getTextContent(), CRON_SCHEDULE))
SCHEDULE_NODE.setTextContent(CRON_SCHEDULE)
import javax.xml.transform.*
import javax.xml.transform.dom.*
import javax.xml.transform.stream.*
LOG.println(" Writing ${THIS_JOB_CONFIG}")
Transformer transformer = TransformerFactory.newInstance().newTransformer()
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
transformer.transform(new DOMSource(doc), new StreamResult(new File(THIS_JOB_CONFIG)))
LOG.println(" ${SEPARATOR} Adapted configuration of ${THIS_JOB_NAME} ${SEPARATOR}")
Hints are welcome why println()
doesn't print anything to Jenkin's Console Output in this Post-build Action.
UPDATE: Got it! According to [JENKINS-18651 – Enable correct writing to the job's log from a post build script] it's manager.listener.logger.println()
instead of just println()
.
It should be noted that there are three situations where this solution can cause issues:
config.xml
and a user saves D's configuration via the UI at the same moment → concurrent write access.
config.xml
we can cover this with exception handling and a retry after a few seconds in our scripts.config.xml
we're out of luck → the UI will probably show an error under the regime of the "diabolic" Jenkins:config.xml
at the same moment as D, or vice versa → we can cover this with exception handling and a retry after a few seconds in our scripts.config.xml
and Jenkins' TimerTrigger reads the file (it does that once a minute) at the same moment → reading incorrect data due to write not yet finished completely.I leave it as a challenge for the reader to avoid code duplication in case of more than one upstream job by:
http://jenkins/userContent/scripts/JobCronScheduleConfigurator.groovy
http://jenkins/userContent/scripts/JobCronScheduleConfigurator.groovy
Unfortunately Post-build Action → Groovy Postbuild doesn't support Groovy script file so there remains code duplication in this respect (and the logging, see above, is different anyway).
Upvotes: 2