CI/CD design decisions for a PowerShell project on Travis CI
By Troy Lindsay
- 8 minutes read - 1575 wordsPer my previous post, this post covers my continuous integration / continuous deployment design decisions for my open source ArmorPowerShell project.
General Configuration
Building Specific Branches
You can allowlist and/or denylist branches here, but I chose to build all branches in this project and included logic in the various scripts to limit actions prior to merging into main.
# whitelist
#branches:
#only:
#- main
# blacklist
#branches:
#except:
#-
Jobs
You can granularly define build stages as well as conditional builds based on criteria such as branch, release tag, et cetera in the Jobs section. I have not implemented this so far.
#jobs:
#include:
#- stage:
Language
I recommend setting the Language to generic
for scripting language projects (which is not listed in the Language documentation, but is briefly mentioned here), because all I needed for installing PowerShell Core was bash, curl, & apt for Ubuntu and homebrew for macOS, but there are a wide variety of choices if you require otherwise.
language: generic
Runtime
You can also define specific runtime versions for certain applications. If more than one runtime version is specified for the same item, a job will be created for each version. I did not need to implement any of these for this project though.
#dotnet:
#gemfile:
#mono:
#php:
#python:
#rvm:
Git
In the Git section, you can specify a clone depth limit or disable cloning of submodules to optimize job performance.
As of 20171128, the default commit depth on Travis CI is 50
, which should provide sufficient commit history for most projects with accommodation for job queuing.
#git:
#depth:
#submodules:
Environment Configuration
Environment Variables
If you plan to test your open-source PowerShell project on multiple CI providers such as Travis CI and AppVeyor, I recommend defining a few global environment variables such as the ones listed below that abstract the CI specific variables to minimize the logic needed for handling each in your build scripts. If you define a variable more than once, another job will be created for each definition. You can also define matrix-specific environment variables in this section, or at the image level in the Matrix section.
# environment variables
env:
global:
- BUILD_PATH="$TRAVIS_BUILD_DIR"
- MODULE_NAME="<insert module name>"
- MODULE_PATH="$BUILD_PATH/$MODULE_NAME"
- MODULE_VERSION="{set module version in build script}"
- OWNER_NAME="$(echo $TRAVIS_REPO_SLUG | cut -d '/' -f1)"
- PROJECT_NAME="$(echo $TRAVIS_REPO_SLUG | cut -d '/' -f2)"
- secure: <secure string>
#matrix:
Services
There are lots of terrific services and databases that are installed and available in each image should you need them.
# enable service required for build/tests
#services:
#- cassandra # start Apache Cassandra
#- couchdb # start CouchDB
#- elasticsearch # start ElasticSearch
#- memcached # start Memcached
#- mongodb # start MongoDB
#- mysql # start MySQL
#- neo4j # start Neo4j Community Edition
#- postgresql # start PostgreSQL
#- rabbitmq # start RabbitMQ
#- redis-server # start Redis
#- riak # start Riak
Global Image Settings
You can define your build images at the global scope; however, I chose to use the matrix build image configuration as recommended here for multiple operating system build configurations, because it is cleaner.
For example, when osx_image
is defined at the global scope, your Ubuntu builds will receive the xcode
tag, even though it does not apply.

xcode tag assigned to Ubuntu Trusty build image
# Build worker image (VM template)
#os:
#- linux
#- osx
#sudo: required
#dist: trusty
#osx_image: xcode9.1
Build Matrix
The Matrix section allows you to customize each image that will build your code. I cover most of these features sufficiently in the previous post, but the two that I did not are:
- allow_failures, which will permit the specified build image to pass regardless of any errors that occur. I’ll likely never use this feature because it defeats the purpose of implementing continuous integration in my opinion.
- exclude, which prevents building specified images when you define combinations of environment variables, runtime versions, and/or matrix images. I don’t foresee my scripting language projects being complicated enough to require this feature.
matrix:
include:
- os: linux
dist: trusty
sudo: false
addons:
apt:
sources:
- sourceline: "deb [arch=amd64] https://packages.microsoft.com/ubuntu/14.04/prod trusty main"
key_url: "https://packages.microsoft.com/keys/microsoft.asc"
packages:
- powershell
- os: osx
osx_image: xcode9.1
before_install:
- brew tap caskroom/cask
- brew cask install powershell
fast_finish: true
#allow_failures:
#exclude:
Add-Ons
In the addons section, you can define hostnames, prepare for headless testing, upload build artifacts, add SSH known hosts, et cetera. I have not needed any of these so far for this project.
#addons:
#artifacts:
#paths:
#-
#chrome:
#firefox:
#hosts:
#mariadb:
#rethinkdb:
#sauce_connect:
#username:
#access_key:
#ssh_known_hosts:
APT Add-ons
To install packages not included in the default container-based-infrastructure you need to use the APT addon, as
sudo apt-get
is not available.
For now, I have only used this to setup the Microsoft PowerShell Core package management repository and install PowerShell Core on my Ubuntu Trusty container image defined in my build matrix.
If the APT Add-ons step exits with a non-zero error code, the build is marked as error and stops immediately.
#addons:
#apt:
#sources:
#- sourceline:
#key_url:
#packages:
#-
Build Cache
You can cache files and folders to preserve them between builds such as if you have low-volatility, large files that take a while to clone. I did not. Tabula rasa.
If the cache step exits with a non-zero error code, the build is marked as error and stops immediately.
# build cache to preserve files/folders between builds
#cache:
Before Install
In a
before_install
step, you can install additional dependencies required by your project such as Ubuntu packages or custom services.
One important thing to be aware of is that matrix image instructions override global instructions. Since I placed the homebrew commands to install PowerShell in the Before Install step of the macOS build matrix image, if I were to define a global Before Install step, the macOS build matrix image would ignore it. Alternatively, you could use conditional logic in the global step if you only wanted to perform some instructions on a specific operating system, and some on all build images.
If the before_install step exits with a non-zero error code, the build is marked as error and stops immediately.
#before_install:
Install
As of 20171128, there is no default dependency installation step for PowerShell projects on Travis CI. In the install step, I chose to install and import the necessary PowerShell modules on all build images, and implemented it via a PowerShell script so that I always utilize the same logic in my AppVeyor builds with no additional configuration (ie: DRY).
If the install step exits with a non-zero error code, the build is marked as error and stops immediately.
install:
- pwsh -file ./build/shared/install-dependencies.ps1
Tests Configuration
Before Script
You can run custom commands prior to the build script step. I have not had a need for this step yet.
If the before_script step exits with a non-zero error code, the build is marked as error and stops immediately.
#before_script:
Script
I call both my build script and test runner script here because non-zero error codes flag the build as a failure, but the build continues to run, which was what I wanted for these.
There is also an after_script
section where I could have run my tests, but this step is run last, after the finalization after_success
and after_failure
steps (similar to the AppVeyor on_finish
step), but also after the deploy steps.
Also, these three steps do not affect the build result unless the step times out, and I wanted both the build script and the test script to affect the build result.
script:
- pwsh -file ./build/shared/build.ps1
- pwsh -file ./tests/start-tests.ps1
Before Cache
This step is used to clean up your cache of files & folders that will persist between builds. I have not needed this yet. Again, tabula rasa.
#before_cache:
After Success / After Failure
You can perform additional steps when your build succeeds or fails using the
after_success
(such as building documentation, or deploying to a custom server) orafter_failure
(such as uploading log files) options.
I chose to build my documentation in the build.ps1
script in the script step instead of the after_success
step, because I wanted failure to affect the build result in my project.
# on successful build
#after_success:
# on build failure
#after_failure:
Deployment Configuration
There are tons of continuous deployment options available in the Deployment Configuration, such as Heroku, Engine Yard, and so many others, but I haven’t needed any for this project so far because I’m handling all of the publishing from AppVeyor. The continuous deployment tasks could have been implemented just as easily from Travis CI, I just happened to finish the AppVeyor integration first and my publishing tasks only need to happen once per build.
# scripts to run before deployment
#before_deploy:
#deploy:
#skip_cleanup:
# scripts to run after deployment
#after_deploy:
# after build failure or success
#after_script:
Notifications
It took me approximately one email to get tired of build email notifications. I recommend disabling it in the Notifications section as shown below. Next, there are tons of free options out there, but I chose to create a free Slack.com workspace for monitoring builds. Travis CI has an app published in the Slack App Directory, and setup instructions can be found here.
notifications:
email: false
slack:
secure: <secure string>
Conclusion
That’s it for now. I have really enjoyed using the Travis CI platform so far, and feel much more confident in the quality of my project because of it.
Enjoy!