CircleCI + Bash tips
On this page
- CircleCI tips
- Bash tips
- 1. IDE support
- 2. Bash vs other languages
- 3. Double-quote variables
- 4. Separate
export variable
and assignment - 5. Add
set -eo pipefail
to functions - 6.
[[...]]
vs[...]
- 7. Operator priority
- 8. Return code of the latest command is available in
$?
variable - 9. A ternary operator equivalent for Bash
- 10. Boolean values:
t
andf
instead oftrue
andfalse
- 11. echo vs printf
- 12. Local and CI shell compatibility
- 13. Compatible
sed
in-place editing - 14. Temp directory for local development
- Cheatsheet
CircleCI tips
1. JSON schema
For CI configs, choose CircleCI config.yml
JSON schema in IDE:
2. Split out large scripts
Long / medium size Bash scripts shouldn’t be inlined in CI steps. The scripts have to be moved to separate .sh files and sourced in config.yml like this:
source .circleci/scripts/some-action.sh
This makes config.yml shorter and enables syntax check/highlighting for Bash scripts.
Even though IDEA has support for inline syntax highlighting, it’s still less convenient.
3. Don’t substitute CircleCI pipeline parameters in Bash scripts
Substitution with << pipeline.project.git_url >>
syntax in Bash scripts is done plainly, without any escaping of
whitespace or special chars.
Instead, map them to env variables and use the env variables in Bash scripts:
version: 2.1
parameters:
some-param:
type: string
default: ""
another-param:
type: string
default: ""
jobs:
test:
environment:
# Mapping params here:
someParam: << pipeline.parameters.some-param >>
anotherParam: << pipeline.parameters.another-param >>
gitBranch: << pipeline.git.branch >>
steps:
- run:
name: Test
command: |
echo "someParam: $someParam"
echo "anotherParam: $anotherParam"
echo "gitBranch: $gitBranch"
3.1. Reusing mapped pipeline variables
Most likely, you’ll need to use pipeline variables in various jobs/steps, so not to map them every time, you can do this once for all possible pipeline variables used anywhere in your scripts.
3.1.1. Option 1 - yaml anchor
version: 2.1
parameters:
some-param:
type: string
default: ""
another-param:
type: string
default: ""
pipeline-params:
environment: &pipeline-params
someParam: << pipeline.parameters.some-param >>
anotherParam: << pipeline.parameters.another-param >>
gitBranch: << pipeline.git.branch >>
jobs:
test:
environment:
<<: *pipeline-params
steps:
- run:
name: Test
command: |
echo "someParam: $someParam"
echo "anotherParam: $anotherParam"
echo "gitBranch: $gitBranch"
3.1.2. Option 2 - create a separate initialization command
A separate “init” command can be reused in all jobs. This allows unified preprocessing of params.
version: 2.1
parameters:
some-param:
type: string
default: ""
another-param:
type: string
default: ""
commands:
init:
steps:
environment:
someParam: << pipeline.parameters.some-param >>
anotherParam: << pipeline.parameters.another-param >>
gitBranch: << pipeline.git.branch >>
steps:
- run:
name: Init
command: |
# Transform params if needed
[[ $someParam ]] || someParam="<empty>"
# Propagate the variables to further steps
export someParam
declare -p someParam >> "$BASH_ENV"
export anotherParam
declare -p anotherParam >> "$BASH_ENV"
export gitBranch
declare -p gitBranch >> "$BASH_ENV"
jobs:
test:
steps:
- init
- run:
name: Test
command: |
echo "someParam: $someParam"
echo "anotherParam: $anotherParam"
echo "gitBranch: $gitBranch"
4. Passing env variables between steps
This is needed to pass results of steps further.
Serialize variables and store them to “$BASH_ENV” file, which is automatically “sourced” by CircleCI on the next CI
step. Example:
result="some string ' \"
with special characters"
export result
declare -p result >> "$BASH_ENV"
Here, declare -p result
correctly serializes the variable. The representation may vary depending on the shell:
declare -x result="some string ' \"
with special characters"
# or
export result=$'some string \' "\nwith special characters'
Note: declare
and typeset
commands are synonyms.
5. Passing env variables between jobs
While “$BASH_ENV” file is accessible between steps, it needs explicit propagation between jobs through CircleCI workspaces. Workspaces allow you to store files in one job and restore them in further jobs.
store-bash-env:
steps:
- run:
name: Store "$BASH_ENV" file to the workspace
command: |
mkdir -p /tmp/bash-env
cp "$BASH_ENV" /tmp/bash-env/bash-env.sh
- persist_to_workspace:
root: /tmp/bash-env
paths: [ "bash-env.sh" ]
restore-bash-env:
steps:
- attach_workspace:
at: /tmp/bash-env
- run:
name: Restore "$BASH_ENV" file from the workspace
command: |
file=/tmp/bash-env/bash-env.sh
if [[ -f "$file" ]]; then
echo "Found the file '$file'"
cp "$file" "$BASH_ENV"
source "$BASH_ENV"
fi
6. Running steps on job failure/always
By default, if a job is failing, no further steps are executed. This behavior can be controlled per-step,
with the “when” attribute.
Allowed values are: always
, on_success
, on_fail
(default: on_success
)
- run:
name: Notify on failure
command: sh .circleci/scripts/notify.sh "error"
when: on_fail
The Slack orb uses this exact mechanism to send failure notifications.
7. Slack orb - conditional notifications
If you want to decide dynamically in your CI Bash scripts whether to send a notification to Slack with
the Slack orb, as of now (June 2023) one of the easiest way to
do that is to temporarily unset the values of CIRCLE_BRANCH
and CIRCLE_TAG
env vars before the “slack/notify” step:
CIRCLE_BRANCH_="$CIRCLE_BRANCH"
CIRCLE_TAG_="$CIRCLE_TAG"
declare -p CIRCLE_BRANCH_ >> "$BASH_ENV"
declare -p CIRCLE_TAG_ >> "$BASH_ENV"
echo 'unset CIRCLE_BRANCH' >> "$BASH_ENV"
echo 'unset CIRCLE_TAG' >> "$BASH_ENV"
After the “slack/notify” step, the values have to be restored:
CIRCLE_BRANCH="$CIRCLE_BRANCH_"
CIRCLE_TAG="$CIRCLE_TAG_"
declare -p CIRCLE_BRANCH >> "$BASH_ENV"
declare -p CIRCLE_TAG >> "$BASH_ENV"
Make sure both of those extra steps are called only on success/failure or always, depending on your notification “event” param
Bash tips
1. IDE support
Make sure you have Shell script checking & formatting support in your IDE:
preinstalled plugin in JetBrains IDEs
2. Bash vs other languages
For more advanced data processing, prefer writing scripts in a proper programming language like Python, rather than Bash. However, Bash is a good choice for glue code.
3. Double-quote variables
Always double-quote variables as "$someVariable"
, i.e. put them in string literals to prevent word splitting and
glob expansion. An exception is [[ ... ]]
test statements, where variables can be unquoted.
4. Separate export variable
and assignment
Separate export variable
and variable=<expression>
statements if <expression>
isn’t just a string literal but a
command that can fail.
Otherwise, if the <expression>
fails, export
would override its return code with 0.
5. Add set -eo pipefail
to functions
CircleCI automatically adds set -eo pipefail
to scripts, so they fail fast at the failing line, and piping with |
doesn’t override a non-zero return code.
However, when you call Bash functions this is ignored, so set -eo pipefail
must be added to each function definition.
6. [[...]]
vs [...]
For testing / logical conditions, [[ ... ]]
is preferable over [ ... ]
or test ...
.
7. Operator priority
Outside [[ ... ]]
the &&
and ||
operators have the same priority, so ... && ...
may need to be wrapped
in ( ... )
8. Return code of the latest command is available in $?
variable
9. A ternary operator equivalent for Bash
a=$([[ $b == 'c' ]] && echo t || echo f)
echo "$a"
# Outputs: t
It works because if &&
or ||
operators are applied after a test statement, the set -e
fail fast option doesn’t
have an effect.
10. Boolean values: t
and f
instead of true
and false
Using t
and f
strings as boolean values can be safer than using true
and false
.
This is because there already exist true
and false
Bash builtins returning 0 and 1 codes correspondingly.
11. echo vs printf
echo "$variable"
command is lossy - the printed text may be different from the actual $variable
value. echo
behavior also depends on shell implementation.
A lossless alternative is printf '%s' "$variable"
12. Local and CI shell compatibility
Speaking about shell implementation, try to write your scripts in a compatible way, so that they would give exactly the same results on your development machine in your native shell (e.g. zsh for macOS) and in the CI docker image. This makes development and local testing easier.
13. Compatible sed
in-place editing
There’s a known incompatibility between macOS (BSD) and Linux (GNU) sed
commands in in-place editing mode.
The “in-place” feature has to be implemented manually: store the sed output to a temporary file, then move the temp file
to replace the original one.
14. Temp directory for local development
If you need a temp directory in CI scripts, prefer a .gitignored tmp
directory in your project to the system /tmp
dir.
This is to make development and local testing easier - you would see temp files right in your IDE file tree.