SVG
SVG

Conditional Jobs

In many situations your builds will differ depending on the situation. For example, you'll only want to deploy building a release, or when building from the main branch. And your deployment strategies may differ depending on the environment where you'll be deploying (staging vs. production). For this, you'll need conditions.

Types of Conditions

In MonkeyCI, there are multiple strategies to handle conditional jobs.

  • Conditionally add buils to the job list
  • Use job functions that return nil when you don't want to run them
  • Use action jobs that return skipped

In any case, you'll need to use Clojure scripts for conditions, they are not supported in YAML or similar, since they are considered a more advanced feature.

Job List Manipulation

As mentioned earlier, the last expression in your build script must be the list of jobs to execute. But MonkeyCI also accepts a function in this case. So instead of a fixed list, the last expression can also be a function that, given the build context, returns the list of jobs to run. This opens up the possibility to conditionally add jobs to the list depending on the situation, for example the git ref that was pushed to.

(ns build
  (:require [monkey.ci.build.v2 :as m]))

(def unit-tests
  "Runs the unit tests"
  (-> (m/container-job "unit-tests")
      (m/image "docker.io/maven:latest")
      (m/script ["mvn verify"])))

(def deploy
  "Deploys the lib"
  (-> (m/container-job "deploy")
      (m/image "docker.io/maven:latest")
      (m/script ["mvn deploy:deploy"])
      (m/depends-on ["unit-tests"])))

(defn jobs
  "This function is the last statement and creates the list of jobs"
  [ctx]
  [unit-tests
   ;; Only deploy when on the main branch
   (when (m/main-branch? ctx)
     deploy)])

The above example will always execute the unit-tests job, but will only run the deploy job when triggered from the main branch, which is configured on the repository. MonkeyCI will automatically skip the nil entries in the job list.

Now this opens up lots of possibilities, because you can apply the full Clojure API library to that list. Composing sublists, reordering, anything you need!

Job Functions

Although using the job list function is fairly straightforward, it could lead to a cluttered job list which may be difficult to reason about (although you can still write unit tests for it). An alternative is using job functions. Instead of defining your jobs directly, you can instead use functions that take the build context, and return one or more jobs to add to the build. If a function returns nil, this will simply be skipped and not result in a job in the build.

So the above example may be reworked to look like this:

(ns build
  (:require [monkey.ci.build.v2 :as m]))

(def unit-tests
  "Runs the unit tests"
  (-> (m/container-job "unit-tests")
      (m/image "docker.io/maven:latest")
      (m/script ["mvn verify"])))

(defn deploy
  "Deploys the lib when on main branch"
  [ctx]
  (when (m/main-branch? ctx)
    (-> (m/container-job "deploy")
        (m/image "docker.io/maven:latest")
        (m/script ["mvn deploy:deploy"])
        (m/depends-on ["unit-tests"]))))

[unit-tests
 deploy]

This makes the job list cleaner, puts all the logic in the deploy job. This is especially useful if you're writing an plugin with reusable jobs.

Action Job Return Values

This option is only valid when using action jobs. The status of container jobs is determined by the exit code of their script, a zero value indicates a success, any other value is interpreted as a failure. But with action jobs, there are more possibilities, because you can decide which value to return. Apart from success and failed, you can also return skipped. This will be indicated in the MonkeyCI user interface, which may be more informative, as opposed to not having a job in the build at all.

(ns build
  (:require [monkey.ci.build.v2 :as m]))

(def optional-job
  (m/action-job "optional-job"
    (fn [ctx]
      (if (m/main-branch? ctx)
        (println "I should be doing something here")
	m/skipped))))	

In this example, when building from the main branch, it will print the message and return nil, which is interpreted as success. In any other case, it will return skipped. So in any case, the job is executed, but the action itself indicates to the system that it did anything or not.

Composed Conditions

As your build script gets more complicated and more situations need to be addressed, the conditions will become more complicated as well. Since we're using a programming language, we have the full freedom to extract those conditions in separate functions, perhaps even plugins of their own! Checking the conditions then becomes as simple as calling a function. For example, we could modify one of the above examples as follows:

(ns build
  (:require [monkey.ci.build.v2 :as m]))

(defn changed?
  "True if any file under `src/` has been touched"
  [ctx]
  (m/touched? ctx #"src/.*"))

(def should-deploy? m/main-branch?)

(defn unit-tests
  "Runs the unit tests if sources have changed"
  [ctx]
  (when (changed? ctx)
    (-> (m/container-job "unit-tests")
        (m/image "docker.io/maven:latest")
        (m/script ["mvn verify"]))))

(defn deploy
  "Deploys the lib when on main branch"
  [ctx]
  (when (should-deploy? ctx)
    (-> (m/container-job "deploy")
        (m/image "docker.io/maven:latest")
        (m/script ["mvn deploy:deploy"])
        (m/depends-on (when (changed? ctx) ["unit-tests"])))))

[unit-tests
 deploy]

We have extracted the condition checks into two functions, changed? and should-deploy?, which shows their intent a little better. Then we have also added a condition on the unit-tests job, which will now only run when the source files have changed. Note that we have also added that check on the deploy dependencies, because otherwise the deploy job would only run when source files have changed, since it's dependent on the unit-tests job.

Conclusion

As you can see, conditions give you a lot of power and flexibility to adjust the flow of your builds, or to make them more efficient. Be sure to use unit tests to verify whether the conditions actually behave the way you have intended.

Related articles