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.

  1. 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.
  1. There is minimal maintenance so the system isn't holding you back with another project to maintain.
  1. 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.
  1. 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:

  1. Cilicon.Installer.zip
  2. Cilicon.zip
  3. 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:

  1. Cilicon.app
  2. Access to a code editor or Terminal

Steps

  1. Go to your home directory and create a new YAML file called cilicon.yml
  2. 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
  1. Go to the VM.bundle file
  2. 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.

  1. Create the files inside the Editor Resources folder:
    • post-run.sh
    • pre-run.sh
    • setup-actions.sh
    • start.command
  1. 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

  1. Go to the repository you want to have the self-hosted runner on
  1. On the tab option at the top, select Settings
  1. From the settings menu, expand Actions and select Runners
  1. Press the New self-hosted runner button
  1. Select macOS as the runner image
  1. Choose the correct Architecture, that is ARM64 for the new Apple chipset, and x64 for any older Intel chipset Macs
  1. 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
  1. Go to the /data folder, and update the contents of your files with the information Github has provided. For example:
FilenameContent
RUNNER_DOWNLOAD_URLhttps://github.com/actions/.../actions-runner.tar.gz
RUNNER_TOKENQWERTY24680
RUNNER_SHAABCDE123456
  1. Update the other files with the relevant information:
FilenameContent
RUNNER_LABELSAdd in additional labels for the runner. Usage in runs-on: label1
RUNNER_NAMEWhat do you want the runner to be called and identified in Github
RUNNER_REPOThe 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.

  1. Run the Cilicon.app and you should see a VM window launch
  1. Go through the normal macOS setup, but you don't have to configure all the options
  1. 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.

  1. 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

  1. 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.

  1. 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.

  1. Copy over the Cilicon.app, ~/Developer/CiliconVM/, and the cilicon.yml file to the same locations on your production machine
  1. Launch the Cilicon.app and see the Github self-hosted runner connect to your repo
  1. 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.


Help me write and make more!

You can help me continue to provide valuable content like this. If you found this article helpful, please consider supporting me.

Coffee Pizza Dinner