Automate your build & release with Github Actions

--

Photo by Daniele Levis Pelusi on Unsplash

DevOps, one of the important practices that every developer should follow to extensively optimize their tasks. Clearly, it’s not limited to the topic we are discussing but it’s highly used in the industry.

So you may ask what are those practices? Well, there are a lot of uses cases but one of the common use-case in software development is supposed you are working on a project & a person wants to contribute to your work then how can you verify that his commit does not break the project with your latest commit upon merging. Surely you can verify it manually but it’s highly time-consuming. At this point, you can use some type of automation to automate this process (called CI). You can also use it for deploying your project like creating a new Github release or can publish them to stores (your imagination drives you), this process is called CD (continuous deployment).

Not only Github other platforms like Gitlab, Bitbucket has their own pipelines for automation. There is one from Microsoft (called Azure DevOps) which is similar to Github Actions (iirc).

In this guide, we will be using the one provided by Github called Actions.

Side Note

This is not a typical article on how to do “this”, here I’ll be focusing on concepts so that you then can write your own workflow in ways you want.

I wrote this article with the experience I earned by using Github Actions over the past couple of months. In case, if there is something that has a better version of representing itself feel free to let me know & I’ll update the article.

What is Github Action?

A non-technical description of it would be “It is an API which will trigger a workflow based on any event that occurs in a repository”.

  • workflow: A YAML file (in.github/workflows) that contains some set of jobs that will run for each specified events.
  • events: Eg: a commit/PR made to a repo, creating a new release, forking a repo, etc. are all events.
  • jobs: A job is…. a job which contains a set of commands that will run on a particular OS after some constraints are satisfied.
  • constraints: These are some conditions that should be met in order to run the commands or jobs.
  • commands: Each command is executed on the native shell depending upon the OS you are running the workflow on (we can configure it according to our need).

Each job will be run on a fresh instance of virtual machines maintained by Github themselves (hosted runners). Each hosted runner has a defined hardware configuration (listed here). You can configure your custom runner to run your workflows in a docker container as well.

Also, there are a lot of prebuilt actions you can use from the official marketplace. We will use some of them in further examples.

A basic workflow setup is shown below.

Some key points

  • build is the name of the job.
  • if is the condition that needs to be satisfied for this “job” to run.
  • runs-on is where your workflow will run eg: ubuntu-latest, windows-latest, mac-latest for respective OS.
  • steps are where we declare all the list of commands that we expect to run. Each command has an optional name & is defined in the run block.
  • uses can be used to use a third party custom action (like a library for a project).
  • Using # we can add comments.

github.* is a context that holds specific information about the event which you can use in a workflow. There are a lot of other contexts that provide different information, you can read more about them here (they are our key to information).

Let’s talk more about that if-condition github.event..... So what is it? Basically, it is a webhook payload (you can consider it as a JSON response). Whenever an event occurs it calls a webhook which returns a specific response like for push event (refer here), if I want to access the repository name I can write github.event.repository.full_name (always make sure to add github.event as a prefix). You can use them in commands like echo ${{ github.event.* }} .

Another most used context is “secret”. These are some hidden string you can store in repo settings > Secrets & can use them in the workflow. They are usually credential information, API keys, etc.

You can declare “env” variables (a const) in the workflow (like here) & use it anywhere in the jobs (as shown here). I don’t use it much so I may have very little knowledge about them in general. Also, here is a list of default environment variables GitHub provides (used as$VARIABLE_NAME ).

Basically, everything that you do in a shell or terminal can be done in the run block. We will see them in further examples.

Let’s write some real examples

Each workflow file is stored in .github/workflows (a directory within your repository). To get started, in the repo, go to the Actions tab & create New workflow or set up a workflow yourself -> . Make sure to give it a name that represents what it's set up for.

The following are some examples I’ve used to describe different use cases. Note, these are the workflow files that I’ve used for my projects.

Android

This workflow file is written for one of my Android app.

The app has some private local.properties keys that need to be set up before running build. One of them is an API key & the other is Google sign-in client ID. So in your repository go to Settings > Secrets > New secret & add them respectively (I stored them as TMDB_API_KEY & GOOGLE_CLIENT_WEB_ID ).

Since it is an Android project we can use gradlew module-name:assembleDebug to assemble a debug build. You can run tests (gradlew test) or lint checks (gradlew lint) in your action command as well. Certainly, I don’t have any tests so let’s skip that.

At line #17 & #18, you can see how we are appending the secrets to local.properties file. The run block is currently running in the Power shell but we can change it by adding the shell keyword (explained here).

Once you understand the syntax writing workflows would become so easy.

One important note is, in the workflow file indentation matters. A wrong indent will cause a workflow to not run.

Android (CD)

This is an example of deploying a release build. I took it from one of my library projects which will release it on maven after certain constraints are satisfied.

Note, this example assumes you have knowledge about building & publishing the library on Maven through Sonatype. If you are new & want to know how to set up an automatic library release you can follow this tutorial.

So after setting maven-publish & gradle script for publishing, Android studio creates some task that can trigger a release to Sonatype.

  1. You are required to store ossh username & password details into local.properties.
  2. You need a key.gpg file which will sign the library using key & password whose information is also stored in local.properties (such information should not be leaked).
  3. You run some set of gradle tasks.

We can extract the local.properties keys through Secrets but what about key.gpg file which should be there in the repo directory while signing the build. But, we can’t just upload it to Github as it contains some private information (it somehow needs to be present during the CI build).

Check line #24, this is how I’ve added a key.gpg file to the repo. In short, I went to this website which converts a file to base64 string. I then stored the base64 string in the Secrets & by using a custom action (on line #27 which converts base64 to file) I’m creating that file in the temp directory.

Note, you can also encrypt such files & store it in your repository & then in CI decrypt them with the password (from Secrets). However, if your repo is public people can still try to brute-force that encrypted file which is not safe if you are working on a company project.

Rust

I took this example from one of my Rust projects because this workflow used a concept of creating dynamic environment variables & using them in subsequent jobs.

Let us first read & understand the workflow.

As you see from Line #20, the basic syntax for setting variable is,

# Setting variable
echo "::set-output name=variable_name::value"
# Setting variable based on calculation
echo "::set-output name=variable_name::$(echo run-shell-command)"

Note, the life of the variable is until the job is running.

Rust (CD)

This workflow is also taken from the same repo, it explains the use of strategy & also on how to chain subsequent jobs (dependent job).

By terms, each job in the workflow will run independently from one another. If you want the next job to use the output of the previous job you can chain them.

This workflow will draft a new release & will attach binaries produced from cargo build for various platforms.

See line #9, it shows how to run the same job on multiple platforms. In further steps, you will see how we run a specific command depending on the os.

Tip: You can also use runner context as runner.os to identify the OS you are running the job on.

At line #66 you will see how we chain the job by using the needs keyword. When a job is dependent on the next job at this point the last state of the previous job will be used by the next job i.e the state of the repository will be forwarded to the next job.

Conclusion

Even if Github Actions are free there are some usage limits as discussed over here. This is also well explained in their pricing catalog.

I think this guide should be enough for anyone to get started with actions. You just have to think in actions “Everything runs in a shell” & you are good to go.

Some reference which helped me to learn more about actions are as follows.

👋 Join FAUN today and receive similar stories each week in your inbox! Get your weekly dose of the must-read tech stories, news, and tutorials.

Follow us on Twitter 🐦 and Facebook 👥 and Instagram 📷 and join our Facebook and Linkedin Groups 💬

If this post was helpful, please click the clap 👏 button below a few times to show your support for the author! ⬇

--

--