GitHub Actions
.github/workflows/ci.yml
To get started with GitHub Actions, commit this YAML file into your Git repository under the directory , push it to GitHub, and observe the Actions tab.
Quickstart
Check out to quickly get a config with the CI features you need. Or continue reading for more details.
This runs on GitHub’s default “latest Ubuntu” container. It downloads the source code from the repository itself (directly into the current directory), installs Crystal via , then runs the specs, assuming they are there in the spec/
directory.
If any step fails, the build will show up as failed, notify the author and, if it’s a push, set the overall build status of the project to failing.
Tip
For a healthier codebase, consider these flags for crystal spec
:--order=random
--error-on-warnings
If your test coverage isn’t great, consider at least adding an example program, and building it as part of CI:
For a library:
- name: Build example
run: crystal build examples/hello.cr
For an application (very good to do even if you have specs):
- name: Build
run: crystal build src/game_of_life.cr
By default, the latest released version of Crystal is installed. But you may want to also test with the “nightly” build of Crystal, and perhaps some older versions that you still support for your project. Change the top of the workflow as follows:
jobs:
build:
strategy:
fail-fast: false
crystal: [0.35.1, latest, nightly]
runs-on: ubuntu-latest
steps:
- name: Download source
uses: actions/checkout@v2
- name: Install Crystal
uses: crystal-lang/install-crystal@v1
with:
crystal: ${{ matrix.crystal }}
By specifying the version of Crystal you could even opt out of supporting the latest version (which is a moving target), and only support particular ones.
Typically, developers run tests only on Ubuntu, which is OK if there is no platform-sensitive code. But it’s easy to add another system into the test matrix, just add the following near the top of your job definition:
Installing Shards packages
Most projects will have external dependencies, “shards”. Having declared them in shard.yml
, just add the installation step into your workflow (after install-crystal
and before any testing):
- name: Install shards
run: shards install
If your repository has a checked in shard.lock
file (typically good for applications), consider the effect that this has on CI: shards install
will always install the exact versions specified in that file. But if you’re developing a library, you probably want to be the first to find out in case a new version of a dependency breaks the installation of your library — otherwise the users will, because the lock doesn’t apply transitively. So, strongly consider running shards update
instead of shards install
, or don’t check in shard.lock
. And then it makes sense to add to your repository.
Our application or some shards may require external libraries. The approach to installing them can vary widely. The typical way is to install packages using the apt
command in Ubuntu.
Add the installation step somewhere near the beginning. For example, with libsqlite3
:
- name: Install packages
run: sudo apt-get -qy install libsqlite3-dev
Enforcing code formatting
If you want to verify that all your code has been formatted with , add the according check as a step near the end of the workflow. If someone pushes code that is not formatted correctly, this will break the build just like failing tests would.
- name: Check formatting
run: crystal tool format --check
Consider also adding this check as a Git pre-commit hook for yourself.
We have been using an “action” to install Crystal into the default OS image that GitHub provides. Which has multiple advantages. But you may instead choose to use Crystal’s official Docker image(s), though that’s applicable only to Linux.
The base config becomes this instead:
.github/workflows/ci.yml
Some for containers are crystallang/crystal:nightly
, crystallang/crystal:0.36.1
, crystallang/crystal:latest-alpine
.
Caching
The safe approach is to add the step (before the step that uses shards
) defined as follows:
- name: Cache shards
uses: actions/cache@v2
with:
key: ${{ runner.os }}-shards-${{ hashFiles('shard.yml') }}
restore-keys: ${{ runner.os }}-shards-
- name: Install shards
run: shards update
Important
You must use the separate key and restore-keys. With just a static key, the cache would save only the state after the very first run and then keep reusing it forever, regardless of any changes.
But this saves us only the time spent downloading the repositories initially.
A “braver” approach is to cache the lib
directory itself, but that works only if you fully rely on shard.lock
(see ):
- name: Cache shards
uses: actions/cache@v2
with:
path: lib
key: ${{ runner.os }}-shards-${{ hashFiles('**/shard.lock') }}
- name: Install shards
run: shards check || shards install
Note that we also made the installation conditional on shards check
. That saves even a little more time.
If your project is an application, you likely want to distribute it as an executable (“binary”) file. For the case of Linux x86_64, by far the most popular option is to build and link statically . This means that you cannot use GitHub’s default Ubuntu container and the install action. Instead, just use the official container:
.github/workflows/release.yml
jobs:
release_linux:
runs-on: ubuntu-latest
container:
image: crystallang/crystal:latest-alpine
steps:
- uses: actions/checkout@v2
- run: shards build --production --release --static --no-debug
These steps would be followed by some action to publish the produced executable (), in one of the two ways (or both of them):
As part of the CI done for every commit, via actions/upload-artifact.
Then consider linking to the latest “nightly” build using the external service
Distributing executables for macOS (search for examples) and Windows () is also possible.