We'll be using Github Actions to teach some of the basics.
This is a very broad skill in the world of software engineering and can be customized to fit almost any project.
Credits:
This project makes heavy use of Nektos's act to run challenges in the dojo.
This module was created by Eli Sells as an honors project for CSE 365.
Getting Started
Let's try something really simple to start.
As you'll recall from the videos, the .yml functions almost like a bash script. For this challenge (and only this challenge), /flag will be owned by the runner. The output from the challenge runner will be displayed in stdout. Can you get the flag?
The checker script for the whole module is at /challenge/check. The directory with the workflows is /challenge/repository/.github/workflows. Remember what happens when you try to run ls on a directory with a . in front of it? Don't worry, it's definitely there!
In the first challenge, your workflow ran as root. From now on, we'll be protecting the flag and the only (intended) way for you to complete these challenges will be to pass the provided conditions.
One of the most common ways to utilize a continuous integration system is to build an application
in some kind of reproducible environment. For example, Sun Devil Rocketry builds its flight firmware apps
in a CI pipeline that follows these steps:
Check out the repository's code, and recursively check out the submodules
Install the required dependencies (a version of GCC for bare-metal ARM targets called arm-none-eabi-gcc)
Invoke all flight firmware makefiles for legacy apps
Invoke the current app's makefile in debug mode
Invoke the current app's makefile in release mode
By doing this all in a workflow, we're able to verify that someone's changes won't break the build
on any application that is actively supported by the team.
Because of my own familiarity with C and C++ build systems, that's what we'll have you use here. We have provided a Makefile and a source file. You don't need to know how either of them work,
all you need to do is invoke the Makefile in your workflow! The program it builds will be run
as root, and it will print the flag to stdout.
This challenge also has one way to simplify things: instead of simulating a remote run location, this will run in /challenge/repository. This is necessary to make sure your compiled program exists after the workflow cleans itself up. In future challenges, this will not be the case.
P.S: If you aren't familiar with Makefiles, they also work kinda like an extended version of a shell script. All you have to do is run the make command in the same directory as any file designated as a Makefile.
In the last challenge, your workflow ran in /challenge/repository. Obviously, on a real remote
runner, you would need to be able to set everything up yourself. This might include compilers,
SDKs, other external libraries, etc. Fortunately, since this project is built in C and runs on an ubuntu-latest runner, GCC comes pre-installed (both on our dojo CI run simulator and on actual Ubuntu runners)!
If you're curious about what tools come pre-installed on each runner for your own projects, check out the following resources:
However, nothing new gets downloaded onto your runner without you telling it to, including the code from the repository that triggered the run. There's a pretty easy solution for this, luckily! GitHub packages its own set of steps to do this for you.
- name: Check out repository code
uses: actions/checkout@v4
This step checks out your repository to the root of the runner (the runner's root directory == the repository's root directory). From there, you can proceed as you did previously -- almost. Since we're no longer in /challenge/repository, we can't see your compiled binary once you're done! The runner cleans up after itself. The fix for this is just to run it (remember the binary is called program and lives in the build directory).
P.S: If you aren't familiar with Makefiles, they also work kinda like an extended version of a shell script. All you have to do is run the make command in the same directory as any file designated as a Makefile.
In the last challenge, you built your program on a remote runner! This is a great first step
towards unlocking CI/CD in your own projects, and you've hit the first of the three prongs I
mentioned in the lectures. Now, it's time to look at the second prong.
It's really useful to be able to run automated tests in your CI pipelines. This is a huge step
towards promoting code quality, since every change will verify that core functionality hasn't
been broken. The tests themselves are out of scope for this module, since there's so much to
cover in the world of SQA and testing. Depending on your language or environment, there are
so many different tools for this and we definitely couldn't cover it all as a tangent. If you
don't have any experience with software testing coming into this, I highly recommend you go out
and do some research to see what it's all about.
For this module, we'll keep it simple and just simulate the invocation of a test using a bash
script. You can use your imagination and fill in the blanks based on whatever framework you
might be used to (if this were Sun Devil Rocketry, the command would be "make test").
You'll want to perform two steps in your workflow:
Compile the program, same as before.
Invoke tst/check-sha.sh
The checker script will verify that you ran the test correctly. Good luck!
Great job! Being able to run tests like that is a huge reason why I love CI so much.
Now, let's expand upon this a little bit. Most projects won't just have one test to run.
There are a bunch of ways to run multiple tests, but my favorite is built in to Github Actions
for exactly this purpose. In your .yaml file, you can specify a list of jobs that use the same specification with keywords swapped out. It's hard to explain in text, but I'll show you what I mean here:
jobs:
test-runner:
runs-on: ubuntu-latest
strategy:
matrix:
tests: [test-1, test-2]
steps:
- name: Build and run the app/${{ matrix.tests }} tests
run: |
cd ${{ github.workspace }}/test/app/${{ matrix.tests }}
make test
Note the "strategy: matrix: tests: [...]" here, that's doing a lot of heavy lifting. What happens is the list under "steps" runs twice, once for each item in the list. When it runs, it'll fill in this ${{ matrix.tests }} variable with every value enumerated in the list, letting you re-use your specification with only the replacements you need.
For this challenge, you'll do the same thing. In addition to the tst/check-sha.sh from the last challenge, there's now a tst/check-sha2.sh. You'll invoke it the same way, and the checker script will look for both in your output.
You're nearly there! The last thing we're gonna look at is the basics of CD, or more
accurately: how to link your CI pipeline up to a delivery mechanism. Now, there are
limits to what we can simulate in the dojo, but luckily, our tools allow us to set up a fake
artifact server. So, we're gonna do Build a Binary 2 again, but with one little extra step this
time.
In an actual environment, this would upload a file from your workflow run to Github! I use this
to preserve test artifacts after a run, including results and coverage reports. I'm also
starting to use it for CD, building binaries and making pre-releases for me on Github
automatically. You're going to use it to upload your compiled program, and then the checker
script is gonna run it. Simple!
--
Note: Due to the internet access restriction in the dojo, we can't actually use the real upload
artifacts action. We're gonna get around it by using the following step: