Our pipeline is currently building a Linux binary of our application before adding it to a container image. What if we want to distribute the application also as executables for different operating systems? We could provide that same binary, but that would work only for Linux users since that is the architecture it is currently built for. We might want to extend the reach to Windows and macOS users as well, and that would mean that we’d need to build two additional binaries. How could we do that?

How can we create binaries for all three OS?#

Since our pipeline is already building a Linux executable through a step inherited from the build pack, we can add two additional steps that would build for the other two operating systems. But that approach would result in go-demo-6 binary for Linux, and our new steps would, let’s say, build go-demo-6_Windows and go-demo-6_darwin. That, however, would result in “strange” naming. In that context, it would make much more sense to have go-demo-6_linux instead of go-demo-6. We could add yet another step that would rename it, but then we’d be adding unnecessary complexity to the pipeline that would make those reading it wonder what we’re doing. We could build the Linux executable again, but that would result in duplication of the steps.

A better solution is to remove the build step inherited from the build pack and add those that build the three binaries in its place. That would be a more optimum solution. One step removed, three steps added. But those steps would be nearly the same, the only difference would be an argument that defines each OS. Instead of having three steps, one for building a binary for each operating system, we’ll create a loop that will iterate through values that represent operating systems and execute a step that builds the correct binary.

This might be too much to swallow at once, so we’ll break it into two tasks. First, we’ll try to figure out how to remove a step from the inherited build pack pipeline. If we’re successful, we’ll put the loop of steps in its place.

Let’s get started.

We can use the overrides instruction to remove or replace any inherited element. We’ll start with the simplest version of the instruction and improve it over time.

Overriding the release pipeline#

Please execute the command that follows to create a new version of jenkins-x.yml.

All we did was to add two lines at the end of the pipeline. We specified that we want to override the release pipeline.

Just as with the previous examples, we’ll validate the syntax, push the changes to GitHub, and observe the result by watching the activities.

The output of the last command, limited to the relevant parts, is as follows.

Judging from the output of the latest activity, the number of steps dropped drastically. That’s the expected behavior since we told Jenkins X to override the release pipeline with nothing. We have not specify replacement steps that should be executed instead of those inherited from the build pack. So, the only steps executed are those related to Git since they are universal and not tied to any specific pipeline.

Please press ctrl+c to stop watching the activities.

Overriding the build stage of the release pipeline#

In our case, overriding the whole release pipeline might be too much. We do not have a problem with all of the inherited steps, but only with the build stage inside the release pipeline. So, we’ll override only that one.

Since we are about to modify the pipeline yet again, we might want to add the rollout command to the release pipeline as well. It’ll notify us if a release cannot be rolled out.

Off we go.

We added the stage: build instruction to the existing override of the release pipeline. We also added the rollout command as yet another step in the promote stage of the release pipeline.

You probably know what comes next. We’ll validate the pipeline syntax, push the changes to GitHub, and observe the activities hoping that they will tell us whether the change was successful or not.

The output, limited to the latest build, is as follows.

The first thing we can note is that the number of steps in the activity is closer to what we’re used to. Now that we are not overriding the whole pipeline but only the build stage, almost all the steps inherited from the build pack are there. Only those related to the build stage are gone, simply because we limited the scope of the overrides instruction.

Please stop watching the activities by pressing ctrl+c.

We are getting closer to our goal. We just need to figure out how to override a specific step with the new one that will build binaries for all operating systems. But, how are we going to override a particular step if we do not know which one it is? We could find all the steps of the pipeline by visiting the repositories that host build packs. But that would be tedious. We’d need to go to a few repositories, check the source code of the related pipelines, and combine the result with the one we’re rewriting right now. There must be a better way to get an insight into the pipeline related to go-demo-6.

Reverting the changes#

Before we move on and try to figure out how to retrieve the full definition of the pipeline, we’ll revert the current version to the state before we started “playing” with overrides. You’ll see the reason for this soon.

The jx step syntax effective command#

Now that we are back to where we were before we discovered overrides, we can learn about yet another command.

The output is the “effective” version of our pipeline. You can think of it as a merge of our pipeline combined with those it extends (e.g., from build packs). It is the same final version of the YAML pipeline Jenkins X would use as a blueprint for creating Tekton resources.

The reason we’re outputting the effective pipeline lies in our need to find the name of the step currently used to build the Linux binary of the application. If we find its name, we will be able to override it.

The output, limited to the relevant parts, is as follows.

We know that the step we’re looking for is somewhere inside the release pipeline, so that should limit the scope. If we take a look at the steps inside, we can see that one of them executes the command make build. That’s the one we should remove or, to be more precise, override.

You’ll notice that the names of the steps are different in the effective version of the pipeline. For example, the rollout step we created earlier is now called promote-rollout. In the effective version of the pipelines, the step names are always prefixed with the stage. As a result, when we see the activities retrieved from Tekton pipeline runs, we see the two (stage and step) combined.

There’s one more explanation I promised to deliver.

🔍 Why did we revert the pipeline to the version before we added overrides?#

If we didn’t revert the pipeline, we wouldn’t be able to find the step we were looking for. The whole build stage from the release pipeline would be gone since we had it overridden to nothing.

Now, let’s get back to our mission. We know that the step we want to override in the effective version of the pipeline is named build-make-build. Since we know that the names are prefixed with the stage, we can deduce that the stage is build and the name of the step is make-build.

Now that it’s clear what to override, let’s talk about loops.

Adding the loop#

We can tell Jenkins X to loop between values and execute a step or a set of steps in each iteration. An example of the syntax could be as follows:

If we had that loop inside our pipeline, it would execute a single step five times, once for each of the values of the loop. What we put inside the steps section is up to us, and the only important thing to note is that steps in the loop use the same syntax as the steps anywhere else (e.g., in one of the stages).

Now, let’s see whether we can combine overrides with loop to accomplish our goal of building a binary for each of the “big” three operating systems.

Please execute the command that follows to update jenkins-x.yml with the new version of the pipeline.

This time we are overriding the step make-build in the build stage of the release pipeline. The “old” step will be replaced with a loop that iterates over the values that represent operating systems. Each iteration of the loop contains the GOOS variable with a different value and executes the command that uses it to customize how we build the binary. The end result should be go-demo-6_ that is executable with the unique suffix that tells us where it is meant to be used (e.g., linux, darwin, or windows)s.

🔍 If you’re new to Go, the compiler uses environment variable GOOS to determine the target operating system for a build.

Next, we’ll validate the pipeline and confirm that we did not introduce a typo incompatible with the supported syntax.

Changing the reference in the Dockerfile#

There’s one more thing we should fix. In the past, our pipeline was building the go-demo-6 binary, and now we changed that to go-demo-6_linux, go-demo-6_darwin, and go-demo-6_windows. Intuition would tell us that we might need to change the reference to the new binary in Dockerfile, so let’s take a quick look at it.

The output is as follows.

The last line will copy all the files from the bin/ directory to the container root. This would introduce at least two problems. First of all, there is no need to have all three binaries inside the container images we’re building. That would make them bigger for no good reason. The second issue with the way binaries are copied is the ENTRYPOINT. It expects /go-demo-6, instead of go-demo-6_linux that we are building now. Fortunately, the fix for both of the issues is straightforward. We can change the COPY instruction in Dockerfile so that only go-demo-6_linux is copied and that it is renamed to go-demo-6 during the process. That will help us avoid copying unnecessary files and will still fulfill the ENTRYPOINT requirement.

Pushing changes and observing the activities#

Now we’re ready to push the change to GitHub and observe the new activity that will be triggered by that action.

The output, limited to the latest build, is as follows.

We can make a few observations. The Build Make Build step is now gone, so the override worked correctly. We have Build1, Build2, and Build3 in its place. Those are the three steps created as a result of having the loop with three iterations. Those are the steps that are building windows, linux, and darwin binaries. Finally, we can observe that the Promote Rollout step is now shown as succeeded, thus providing a clear indication that the new building process (steps) worked correctly. Otherwise, the new release could not roll out, and that step would fail.

Please stop watching the activities by pressing ctrl+c.

Before we move on, I must confess that I wouldn’t make the same implementation as the one we just explored. Instead, I’d rather change the build target in Makefile. That way, there would be no need for any change to the pipeline. The build pack step would continue building by executing that Makefile target so there would be no need to override anything, and there would certainly be no need for a loop. Now, before you start throwing stones at me, I must also state that overrides and loop can come in handy in some other scenarios. I had to come up with an example that would introduce you to overrides and loop, and that ended up being the need to cross-compile binaries, even if it could be accomplished in an easier and a better way. Remember, the “real” goal was to learn those constructs, and not how to cross-compile with Go.


Next, let’s see how we can work with pipelines without buildpacks.

Working with Environment Variables and Agents
Pipelines Without Buildpacks
Mark as Completed
Report an Issue