When developing software solo, you don’t have to spend time communicating your decisions to anyone, and you’re likely to format your code today the way you formatted it yesterday. However, when working in a shared codebase, it becomes much more important to create and follow standards of behavior. Having a shared understanding of “the way we do things” ultimately saves time by eliminating unnecessary back-and-forth, confusing code reviews, and having to re-do PRs on incorrect branches.
However, new rules and standards can be difficult to adopt. CircleCI can help by automating the process and creating built-in alerts when standards are violated. Read on for three ways CircleCI can help you enforce build standards on your team.
1: Enforce Code Formatting
When developing software with a team, it’s important to ensure code is formatted consistently between contributors.
Different styles used by different developers can lead to more difficult code reviews and increased complexity in maintaining the software afterwards.
There are text editor plugins and even CLI tools that can be used to enforce code formatting, however their success is reliant on devs actually remembering to use them. CircleCI can automate the enforcement of code formatting. To show how this would work, there’s no better example than with the Go (Golang) programming language.
The Go Toolchain comes with a handy command called go fmt
(a wrapper for gofmt
) that formats Go code according to the community’s agreed-upon standards.
Running this command in a CircleCI build step, with a little bit of Bash, can purposely fail a build when code hasn’t been formatted properly.
Here’s an example .circleci/config.yml
step:
#...
- run:
name: "Enforce Go Formatted Code"
command: "! go fmt ./... 2>&1 | read"
#...
For the curious, here’s the breakdown of what’s happening:
go fmt
is the command to check the code for formatting and return properly formatted code if necessary../...
is a special syntax telling the Go tool to check this package (typicallymain
) and any subpackages found there.2>&1
redirects error messages to standard out. Useful for the next step.| read
reads output on standard out. Ifgo fmt
finds anything to correct or has an error, it will be outputted toread
.read
will return an error if it doesn’t get any output to read.!
this flips the error code.read
exits on 0 when there’s output, meaning there’s formatting to be done. We reverse this to a fail because that means the dev didn’t format their code. Instead, we want an exit code of 0 when there’s no output; this means the code is formatted and ready.
Not all languages come with code formatting tools but fortunately third-party tools exist.
2: Enforce Build Time Limits
For larger companies, build time is a key metric.
Keeping track of build time not only helps managers track how efficient their software pipeline is, but can also expose issues that may surface as random spikes in build time.
These issues can be found by browsing CircleCI Insights or a third-party dashboard, but, realistically, how often will that be done?
Instead, we can surface these issues more directly by explicitly causing builds to fail when they go over a pre-determined amount of time.
For example, if a project seems to have an average build time (you can easily determine this with CircleCI Insights) of 2 minutes and 10 seconds, we could set a hard limit of 5 minutes. At more than double our average build time, if a build suddenly takes longer than 5 minutes, we fail it in order to have a human review the build and see what’s going on.
Here’s an example .circleci/config.yml
snippets to accomplish this.
The first step should be at the very beginning of a job:
#...
- run:
name: "Set 5m Build Time Limit"
command: sleep 300; touch .fail-build; exit 1
background: true
#...
While the second step should be at the very end:
#...
- run:
name: "Enforce Build Time Limit"
command: if [ -f .fail-build ]; then echo "Build ran too long."; exit 1; fi
when: always
#...
With the first step, sleep
is being run in a background process at the amount of time we’d like to fail builds at.
In this instance, that time is 5 minutes, or 300 seconds.
If the sleep
command ever completes, that means the build is still running, so a file .fail-build
is created.
The second step looks for the .fail-build
file.
If it’s found, the build ran too long and we purposely return exit 1
in order to fail the build.
If it’s not found, the build time is under 5 minutes and we can exit normally.
This tip is also useful for PRs where you’d want to make sure that changes aren’t significantly adding build time.
3: Enforce a PR Target Branch
Many projects have processes in place for code contribution. In open-source projects in particular, there are typically requirements to sign a CLA, code formatting (see tip #1), and rules on which branches PRs should be based on and/or targeted. The latter is something that can be easily enforced in CircleCI.
Every now and then, I’ll come across a project that states in their CONTRIBUTING.md
file that all PRs must target the develop
branch and not master
. Here’s a CircleCI config step enforcing that very rule:
#...
- run:
name: "Enforce PR Target Branch"
command: |
if [[ -n ${CIRCLE_PR_NUMBER} ]]; then
url="https://api.github.com/repos/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pulls/$CIRCLE_PR_NUMBER?access_token=$GITHUB_TOKEN"
target_branch=$( curl "$url" | jq '.base.ref' | tr -d '"' )
if [[ $target_branch != "develop" ]]; then
echo "This PR is targeting ${target_branch} instead of 'develop'. Failing build."
exit 1
fi
fi
#...
In this example, $CIRCLE_PR_NUMBER
is checked to see if a PR is being built.
If this is a PR, the jq
command is used to parse the GitHub API and determine the target branch for this PR.
The newly created $target_branch
variable is then compared to the desired branch string, develop
in this case, to make sure they match.
If they don’t, we fail the build.
Some notes:
- Change ‘develop’ to whatever your target branch may be.
- A GitHub API token needs to be created and stored as a private environment variable called
$GITHUB_TOKEN
in this case. If this is a public project, make sure to scope this API token specifically to this project and the least permissions possible (just read permission). - This example uses the
jq
command. This command is pre-installed on all CircleCI Convenience Images as well as all CI Builds (2nd-party) Docker images. If you’re using an image withoutjq
installed, their GitHub Readme provides instructions.
These are by no means an exhaustive list of ways to standardize builds across your team. With this post, I aim to give you an idea of what kinds of practices are possible, and ways to automate those practices with CircleCI. What are some of the ways your team enforces standardization? Let me know on Discuss or on Twitter.