Here at Fleet, we make extensive use of GitHub Actions, and we want to share some tips that we’ve learned along the way. These tips can help save time and money while getting the most value from GitHub Actions.
The build matrix is a convenient way to run across combinations of environments. Try performing preparatory work in a separate job that only runs once per workflow, then fanning out to the build matrix
Consider the naive matrix strategy (pseudocode):
for each environment combination: install dependencies build frontend build backend run integration tests
In a complex project like Fleet, installing and building takes a substantial portion of the execution time. That execution time is multiplied across each environment. For our projects using Go, it’s easy to cross-compile for various platforms, meaning that only the
run integration tests step needs to be run on each platform.
Instead, we can use the improved strategy (pseudocode):
install dependencies build frontend build backend upload artifacts for each environment combination: download artifacts run integration tests
We have found uploading and downloading artifacts to be very fast, and we’ve seen substantial reductions in the total time taken by our workflows when using this strategy. Check out the GitHub documentation on artifacts to see how to achieve this.
You can save time in your job by pulling dependencies while performing other work. In our end-to-end testing job, we start Docker dependencies while building the code so that the network requests can complete in the background and the containers are ready for the tests to begin.
Note that background processes do not exit at the end of the step — They keep running until the whole job completes. With
docker compose, using the
-d flag runs the containers in the background. For any other bash command, use
& to run in the background. For example:
- name: Start Docker Compose (background) run: docker compose up -d - name: Other work (background) run: ./do-work.sh &
It’s often helpful to expose a server running inside the job to the internet. We do this for manual QA from our web browser, or to connect clients from one job to a server running in another job. localtunnel makes this easy.
To start a tunnel on port 1337 in the background, use
& as described above.
- name: Start tunnel run: | npm install -g localtunnel lt --port 1337 &
Look in the step output for the assigned hostname, or consider using the
--subdomain flag to request a consistent hostname so that other jobs can connect.
This configuration will cause the Action to run twice for every push to a pull request, doubling the cost if you pay for GitHub Actions:
on: push: pull_request:
One commit, two Workflow runs
Instead, consider running only on pushes to the default branch (
on: push: branches: - main pull_request:
If you’ve ever tried to debug a long-running job step but couldn’t see the output you need for debugging, this seems to be a known issue with GitHub Actions. You can work around it by canceling the step or waiting for it to complete.
Where is the output for Steps 1 & 2?
After cancel, full output is available.
Sometimes getting a shell on the machine is the easiest way to figure out why things are going wrong. Use
mxschmitt/action-tmate to SSH into the GitHub Actions runner. It’s a simple addition to the job steps:
- name: Debug via SSH uses: mxschmitt/action-tmate@8b4e4ac71822ed7e0ad5fb3d1c33483e9e8fb270 # v3.11
When the Action reaches this step, an SSH server starts, and connection information is printed to the output in the Checks tab every 5 seconds (the repetitive output is intended to work around the scrollback issue described above).
It’s also possible to start the SSH server at the end of the job when one of the steps has failed:
- name: Debug on failure if: failure() uses: mxschmitt/action-tmate@8b4e4ac71822ed7e0ad5fb3d1c33483e9e8fb270 # v3.11
Fleet is building an open future for device management, starting with the most widely deployed osquery fleet manager.