Intro

The free CIs are having a difficult time and mostly it’s because of malicious people are abusing to run cryptomining. As a result, TravisCI announced changes in their billing system and GitLab also announced restrictions. So, if we look for other alternatives, Azure Pipelines and Github Actions are the first ones that come to my mind. For the latter, I’ve been playing with it and I’m quite sold on the promise of having everything on Github.

Edit: Just one day after I wrote this down, LayerCI wrote an article about continuous integration and crypto mining. I recommend you to check on it.

Simple CI example for a Rust project

Firstly, I would like to start with a simple example for a Rust project.

name: Build/Test

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

env:
  CARGO_TERM_COLOR: always

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Build
      run: cargo build --verbose
    - name: Run tests
      run: cargo test --verbose

Github already guides you towards creating this simple action yaml and since the project doesn’t have external dependencies or systems integrated into it. We can immediately use it and make the building and testing procedure insanely trivial.

I’m aware that there are other action repositories for rust and cargo but the GitHub vms automatically provides rust compiler and cargo which satisfies the simple necessity. But for using the nightly compiler or other versions or rust, probably that would be the way to go.

The YAML explains itself but in a closer look, we can immediately see that this actions run on pushes or pull requests on main and uses an ubuntu vm.

If you want to check it in action, I apply this action to one of my research related repository.

A more complex CI example with some other system integration

I have some other project in python and it has a MySQL/MariaDB integration. Upon my research, I managed to find that jobs can have services that can be set with something like this

services:
    mariadb:
      image: mariadb:latest
      ports:
        - 3306
      env:
        MYSQL_USER: user
        MYSQL_PASSWORD: password
        MYSQL_DATABASE: test
        MYSQL_ROOT_PASSWORD: password
      options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3

However, I couldn’t manage to make the DB work. I tried many options, but always getting MySQL service is unhealthy. So in the end, I decided to use an external GitHub action.

Its own documentation is pretty clear so won’t be explaining it much. So my GitHub action ci YAML looked like this.

name: Test

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [3.7, 3.8, 3.9]

    steps:
    - uses: getong/mariadb-action@v1.1
      with:
        mysql database: 'dpem_test'
        mysql root password: 'password' 
        mysql user: 'root' 
        mysql password: 'password'
    - uses: actions/checkout@v2
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v2
      with:
        python-version: ${{ matrix.python-version }}
    - name: Install dependencies
      run: |
        sudo apt-get install parallel
        python -m pip install --upgrade pip
        python -m pip install mysql-connector
        if [ -f requirements.txt ]; then pip install -r requirements.txt; fi        
    - name: Run tests
      run: |
        cd tests
        ./run_tests.sh        

As seen above, it’s easy to set a matrix of experiments with the strategy tag and we can use 3 different versions of python without too much of a hassle.

We can also see that we can gather other dependencies from apt-get and/or pip which are necessary for our project.

Again, this GitHub action can be seen in action in a repository

Automated releases with Github Actions

So, I believe this is where the more interesting things began. Using GitHub actions, it’s possible to use the compiled assets to a draft release. And, the best thing is we can do this on 3 different platforms and automatically generate our release binaries for Linux, windows and macos. I know that this is also possible on Azure Pipelines but I only have tested this on GitHub actions and it looks pretty easy to configure.

For the first rust project, the YAML for automated releases looks like this:

name: Create Release Builds

on:
  push:
    tags:
      - "v*" # matches v1.0.1, v1.2, v2, etc
env:
  CARGO_TERM_COLOR: always
  
jobs:
  once:
    name: Create GitHub release
    runs-on: ubuntu-latest
    outputs:
      upload_url: ${{ steps.create_release.outputs.upload_url }}
    steps:
      - name: Create a release
        id: create_release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: Release ${{ github.ref }}
          draft: true
          prerelease: true

  build:
    name: Create cross-platform release build, tag and upload binaries
    needs: once
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    steps:
    - name: Checkout
      uses: actions/checkout@v2
    - name: Build release version
      run: cargo build --release
    - name: Make Zip
      run: 
        7z a -tzip ${{ github.workspace }}/target/release/dig-rs-${{ matrix.os }}.zip ${{ github.workspace }}/target/release/dig-rs* "-x!*.d" "-x!*.pdb"
    - name: Upload Release Asset
      id: upload-release-asset
      uses: actions/upload-release-asset@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ needs.once.outputs.upload_url }}
        asset_path: ${{ github.workspace }}/target/release/dig-rs-${{ matrix.os }}.zip
        asset_name: dig-rs-${{ matrix.os }}.zip
        asset_content_type: application/octet-stream

First of all, this only gets triggered with certain tagged commits. A simple “v*” regex allows any commit with a tag with the prefix “v” (like v1.0 v2.5) valid for this action.

The procedure has 2 distinct jobs. The first job creates an empty draft release and sets up an output upload url for any other job to submit assets.

upload_url: ${{ steps.create_release.outputs.upload_url }}

Specifically, this line sets the upload url as seen in its name. It sets up its upload as one of its steps’ (its id is create_release) outputs.

The second job specifies that it needs the first job (named once) and also defines the 3 different os platforms with the strategy tag.

strategy:
  matrix:
    os: [ubuntu-latest, macos-latest, windows-latest]

Afterwards, we can compile our binary with cargo build --release. Before giving assets to the release, it’s useful to compress the binaries and the nice thing I found while searching online that 7z is available on all 3 platforms. This allows using one command to create one easily selectable zip file depending on the platform.

7z a -tzip ${{ github.workspace }}/target/release/dig-rs-${{ matrix.os }}.zip ${{ github workspace }}/target/release/dig-rs* "-x!*.d" "-x!*.pdb"

The regex dig-rs* allows to collect both dig-rs or dig-rs.exe binary depending on the platform. The necessary exclusion for .d or windows specific .pdb files can be done by "-x!*.d" "-x!*.pdb" on 7z. For more options check 7z documentation.

Afterwards, we can use the release asset action and upload it to the previously defined needed job’s upload path.

That’s all! Again this action is available on one of the myrepositories.

Final words

In my opinion, Github made continuous integrations systems even more convenient to use on projects for multiple purposes. And now with easily set up automated releases, I can automatically compile binaries for my projects for multiple platforms. All I need to do afterwards is to approve the release in the end.

Unfortunately, Github actions is also constantly abused by malicious pull requests for cryptomining and as a result recently, Github restricted automated ci runs for unknown pull requests. It’s not a final solution but it’s a first step towards something.