Setting up Cilicon, VMs, and Github Actions
After the last article, I thought it would be good to show how to set up a Cilicon system to run your Github actions.
But what exactly is Cilicon compared to the other options available when creating a self-hosted runner? From Cilicon's repo:
Cilicon is a macOS App that leverages Apple's Virtualisation Framework to create, provision and run ephemeral virtual machines with minimal setup or maintenance effort. You should be able to get up and running with your self-hosted CI in less than an hour.
This is great because it allows us to achieve a few things without compromise.
- Ephemeral virtual machines means that they are designed to be built and torn down. They aren't like normal VMs which are designed to be long term build.
- There is minimal maintenance so the system isn't holding you back with another project to maintain.
- It seems Cilicon is designed exactly for this purpose and leveraging the Apple Virtualisation Framework meaning it is baked right into the OS, and no longer having to rely on third party systems.
- It uses the APFS file system allowing the cloning and design of the system to be very quick even on large images
Cilicon components
The process took me a little longer than I hoped to set it up, but once I had it all configured the process is actually very simply.
There are three things you need to download from the releases page:
Cilicon.Installer.zip
Cilicon.zip
VM.Resources.zip
CiliconInstaller.app
This part is well documented on the Github README file, but I'll cover it again for prosperity.
This app is used to build the initial VM. When you run the CiliconInstaller.app
it will ask you where to create the .bundle
VM as well as the size of the image.
Cilicon.app
You use the Cilicon.app
to launch your VM, and begin the instance of it. However, at this point you are not ready to do this.
First you need to set up the VM, create the cilicon.yml
file, and prepare your image for deployment.
Setting up Cilicon
For this example, I will use the username markbattistella
and the install location to be /Users/markbattistella/Developer/CiliconVM/
, while using the Terminal app to create and edit files. Before you set it up, ensure you have:
Cilicon.app
- Access to a code editor or Terminal
Steps
- Go to your home directory and create a new YAML file called
cilicon.yml
- Add in the following info to the
cilicon.yml
file:
provisioner:
type: none
hardware:
ramGigabytes: 16
connectsToAudioDevice: false
directoryMounts:
- hostPath: ~/Developer/CiliconVM/VM Cache
guestFolder: Cache
vmBundlePath: ~/Developer/CiliconVM/VM.bundle
numberOfRunsUntilHostReboot: 20
editorMode: true
autoTransferImageVolume: /Volumes/Cilicon Drive
- Go to the
VM.bundle
file - Edit the contents of it by
right-click > Show Package Contents
Inside you will see two folders - Editor Resources
and Resources
. Editor Resources are the files you can use when editorMode:
is true
. Meanwhile the Resources folder is when you are in production.
- Create the files inside the Editor Resources folder:
post-run.sh
pre-run.sh
setup-actions.sh
start.command
- Create the folder called
data
with the files inside (note there are no extensions):RUNNER_DOWNLOAD_URL
RUNNER_LABELS
RUNNER_NAME
RUNNER_REPO
RUNNER_TOKEN
RUNNER_SHA
What have we just created? What our aim is that the start.command
file will launch when the VM boots. Inside that command it will call on the setup-actions.sh
file.
That file reads the contents of the files inside the /data
folder to use as variable values. We do it this way so that if you need to update the TOKEN or a new Github download URL it is not hard coded into the script.
This also allows better versioning or control if using .gitignore
for the Cilicon installer.
We also have the pre-run.sh
and the post-run.sh
files which will be added into the build and tear down of the VM from the setup-actions.sh
file.
Config file data
The configuration files in the bash script compiles the Github self-hosted runner silently. This means that each time the system boots and connects there is no need for the you to interact with the Terminal.
This is what I have in my setup-actions.sh
file:
#!/bin/bash
# -- get the info files
RUNNER_TOKEN=./data/RUNNER_TOKEN
RUNNER_SHA=./data/RUNNER_SHA
RUNNER_NAME=./data/RUNNER_NAME
RUNNER_DOWNLOAD_URL=./data/RUNNER_DOWNLOAD_URL
RUNNER_REPO=./data/RUNNER_REPO
RUNNER_LABELS=./data/RUNNER_LABELS
RUNNER_GROUP=./data/RUNNER_GROUP
# set up internal labels
INTERNAL_LABELS="self-hosted,macOS,ARM64"
# -- check if necessary files exist
# -- check if the contents are not empty
# -- if all pass, assign to variable
if [ -f "$RUNNER_TOKEN" ] && [ -s "$RUNNER_TOKEN" ]; then
RUNNER_TOKEN=$(cat "$RUNNER_TOKEN")
else
echo "RUNNER_TOKEN file is missing or empty. Exiting script."
exit 1
fi
if [ -f "$RUNNER_SHA" ] && [ -s "$RUNNER_SHA" ]; then
RUNNER_SHA=$(cat "$RUNNER_SHA")
else
echo "RUNNER_SHA file is missing or empty. Exiting script."
exit 1
fi
if [ -f "$RUNNER_NAME" ] && [ -s "$RUNNER_NAME" ]; then
RUNNER_NAME=$(cat "$RUNNER_NAME")
else
echo "RUNNER_NAME file is missing or empty. Exiting script."
exit 1
fi
if [ -f "$RUNNER_DOWNLOAD_URL" ] && [ -s "$RUNNER_DOWNLOAD_URL" ]; then
RUNNER_DOWNLOAD_URL=$(cat "$RUNNER_DOWNLOAD_URL")
else
echo "RUNNER_DOWNLOAD_URL file is missing or empty. Exiting script."
exit 1
fi
if [ -f "$RUNNER_REPO" ] && [ -s "$RUNNER_REPO" ]; then
RUNNER_REPO=$(cat "$RUNNER_REPO")
else
echo "RUNNER_REPO file is missing or empty. Exiting script."
exit 1
fi
# -- only check if the labels exist, then use it if it is available
if [ -f "$RUNNER_LABELS" ]; then
IFS=',' read -ra RUNNER_LABELS_ARRAY <<< "$(< "$RUNNER_LABELS")"
IFS=',' read -ra INTERNAL_LABELS_ARRAY <<< "$INTERNAL_LABELS"
UNIQUE_LABELS=$(echo "${INTERNAL_LABELS_ARRAY[@]} ${RUNNER_LABELS_ARRAY[@]}" | tr ' ' '\n' | sort | uniq | tr '\n' ',' | sed 's/,$//')
else
UNIQUE_LABELS="$INTERNAL_LABELS"
fi
# -- only check if the group exist, then use it if it is available
if [ -f "$RUNNER_GROUP" ]; then
RUNNER_GROUP="$(< "$RUNNER_GROUP")"
else
RUNNER_GROUP="default"
fi
# -- download the GitHub action runner
curl -o actions-runner.tar.gz -L $RUNNER_DOWNLOAD_URL
# -- check the checksum
if [ "$(shasum -a 256 actions-runner.tar.gz | awk '{print $1}')" != "$RUNNER_SHA" ]; then
echo "SHA checksum does not match, exiting script"
exit 1
fi
# -- create the runner directory
mkdir -p ~/actions-runner
# -- extract the installer
tar xzf ./actions-runner.tar.gz --directory ~/actions-runner
# -- copy over the pre- and -post commands
cp pre-run.sh ~/actions-runner
cp post-run.sh ~/actions-runner
# -- go to the runner directory
cd ~/actions-runner
# -- add the commands to the runner hooks
export ACTIONS_RUNNER_HOOK_JOB_STARTED=~/actions-runner/pre-run.sh
export ACTIONS_RUNNER_HOOK_JOB_COMPLETED=~/actions-runner/post-run.sh
./config.sh --url "$RUNNER_REPO" --ephemeral --replace --labels $UNIQUE_LABELS --name $RUNNER_NAME --runnergroup "$RUNNER_GROUP" --work _work --token $RUNNER_TOKEN
# Last step, run it!
./run.sh
Setup a self-hosted runner
For this example I will be using a ARM64 image for my M1 MacBook Pro. Please use the code from the Github page and not below as it has been edited or truncated
Github settings
- Go to the repository you want to have the self-hosted runner on
- On the tab option at the top, select
Settings
- From the settings menu, expand
Actions
and selectRunners
- Press the
New self-hosted runner
button
- Select
macOS
as the runner image
- Choose the correct
Architecture
, that isARM64
for the new Apple chipset, andx64
for any older Intel chipset Macs
- On the next screen you will see the set up information if you were to install the runner manually. It will look something like this:
# Create the folder
mkdir actions-runner && cd actions-runner
# Download the latest runner package
curl -o actions-runner-arm64-x.xxx.x.tar.gz -L https://github.com/actions/.../actions-runner.tar.gz
# Validate the hash
echo "ABCDE123456 actions-runner.tar.gz" | shasum -a 256 -c
# Extract the installer
tar xzf ./actions-runner.tar.gz
# Create the runner and start the configuration experience
./config.sh --url https://github.com/user/repo-name --token QWERTY24680
# Last step, run it!
./run.sh
# Use this YAML in your workflow file for each job
runs-on: self-hosted
- Go to the
/data
folder, and update the contents of your files with the information Github has provided. For example:
Filename | Content |
---|---|
RUNNER_DOWNLOAD_URL | https://github.com/actions/.../actions-runner.tar.gz |
RUNNER_TOKEN | QWERTY24680 |
RUNNER_SHA | ABCDE123456 |
- Update the other files with the relevant information:
Filename | Content |
---|---|
RUNNER_LABELS | Add in additional labels for the runner. Usage in runs-on: label1 |
RUNNER_NAME | What do you want the runner to be called and identified in Github |
RUNNER_REPO | The Github URL of the repo where the runner is setup on |
Configure Cilicon VM
Okay at this point, I'm sure you're very eager to launch the VM since we've done all this leg work and have even seen the system run.
- Run the
Cilicon.app
and you should see a VM window launch
- Go through the normal macOS setup, but you don't have to configure all the options
- Once you have completed there are a few things to configure:
- Enable automatic login
- Disable Automatic Software updates
- Disable screen locking or power saving
- Add the
start.command
file as a launch items - Install any dependencies you may need, such as Xcode, Command line tools, brew, etc.
Testing and debugging
At this point I do like to test the start.command
script, by double-clicking it. When I was testing this out I found a few issues which you want to resolve before production.
- Permission denied to run
setup-actions.sh
script
Fix this by running chmod +x setup-actions.sh
in the VM Terminal. This allows the script to be executable
- No keyboard within the VM
This has been noted in the Github issues. The only solution I have found that works is which the VM is booting (the Apple logo) is to repeatedly click and press Spacebar. If it works, you'll see the VM screen flash white.
- Unable to read files
Sometimes if you have edited a file inside the VM or on your host machine, the link makes the file appear corrupt. You can't preview or open it, sometimes deleting it makes it disappear but creating a new file says it already exists. Rebooting the VM solves this.
Testing to Production
If you've run the start.command
script, and everything connects - you are all good to go.
I do a little cleanup before I switch over to production. This involves, deleting the ~/actions-runner
directory, closing all windows and apps, and emptying the Trash.
When you have your VM ready for production, go to your cilicon.yml
file and change the editorMode: true
to editorMode: false
. What this does is any changes made to the VM will not be saved. It also means shutting down the VM will relaunch it instantly - this is by design.
Next steps
The next thing you need to do is find a host machine to run Cilicon. As I've written about before, there are many places you can run this from but you will need to weigh up the pros and cons for your setup.
Ideally you'd have a machine that is always on, connected to the internet, and safe from any interference from programs or people.
- Copy over the
Cilicon.app
,~/Developer/CiliconVM/
, and thecilicon.yml
file to the same locations on your production machine
- Launch the
Cilicon.app
and see the Github self-hosted runner connect to your repo
- Run your repo's
action.yml
file trigger and see it run on the VM
Once it has reached the number of numberOfRunsUntilHostReboot
count, the VM should reboot.
If you found this article helpful, you can keep the ideas flowing by supporting me. Buy me a coffee or check out my apps to help me create more content like this!
Coffee Check out my apps