Continuous Integration with GitHub Actions

Efficient developers implement CI/CD pipelines to automate all sorts of tasks:

  • Linting

  • Testing

  • Code Coverage

  • Code Compilation

  • Deployment

Cloud-based CI/CD services, such as Travis CI and CircleCI, simplify the complex process of automating builds, testing and deployments. Developers define the build environment (e.g., programming language, type and version of runtime engine and operating system) and tasks to execute all within a single configuration file, often formatted using YAML. For Travis CI, there's .travis.yml, and for CircleCI, there's .circleci/config.yml. The configuration file guides the CI/CD service as it provisions virtual machines with the proper environment and runs tasks sequentially and/or in parallel upon triggering certain repository events.

Depending on the event, you may specify a unique workflow that runs its own set of tasks. For example, merging code to the production branch automatically tells the CI/CD service to spin up a new isolated virtual machine and kick off a workflow that builds and deploys your application. Another common event is creating a pull request. This event also automatically tells the CI/CD service to spin up a new isolated virtual machine. However, it may kick off a workflow that lints and tests code and generates a code coverage report.

Workflows can be customized to streamline whatever tedious tasks you (and your collaborators) may forget to run. To remain productive while shipping code with confidence, you should add as many of these tasks to a CI/CD pipeline so that you can focus more on the application code.

If you already have your project hosted on GitHub, then you should consider automating the project's workflow with GitHub Actions, GitHub's CI/CD solution. Choosing GitHub Actions over Travis CI or CircleCI gives you one less third-party service to worry about and store your GitHub token. Despite being launched in late 2018, GitHub Actions' free tier for open-source software (and monthly credits for private repositories) rivals the plans offered by other well-established services. Plus, being natively integrated with GitHub lets you automate issue triaging, hook into any GitHub event and connect with other GitHub platforms and tools like GitHub Packages.

For those who want to programmatically interact with GitHub Actions, the GitHub Actions API provides access to GitHub Actions via a REST API.

Below, I'm going to show you how to set up and run a workflow with GitHub Actions.

Getting Started#

In this tutorial, we will write an automated workflow for a small utility library. The library converts a color from its hexadecimal representation to its RGB representation and vice-versa.

When a contributor creates a new pull request, the workflow should...

  1. Checkout the code.

  2. Install a stable version (latest LTS version) of Node.js.

  3. Install npm dependencies.

  4. Lint the library's code.

  5. Test the library's code.

  6. Generate a code coverage report.

This way, we can check if the changes made in a new pull request unintentionally introduce bugs that break existing functionality even if the submitter forgets to lint and test their changes.

To begin, clone this library to your local machine:

Within the library's directory, delete the .git directory so that you can create your own GitHub repository to try out GitHub Actions.

Configuring the Pull Request Workflow#

To enable a workflow for the library's GitHub repository, create a .github/workflows directory at the root of the library's directory and create a pull-request.yml file within it.

Inside of the .github/workflows/pull-request.yml, let's name the workflow "Pull Request."

(.github/workflows/pull-request.yml)

We want GitHub to trigger this workflow whenever a pull request:

  • Is created.

  • Is reopened (after being closed).

  • Is updated whenever it receives new commits.

Essentially, we want to lint and test the code whenever we make any changes to it.

Using the on workflow syntax, we can define which GitHub event/s trigger the workflow. Events range from repository events (e.g., creating a pull request) to workflow events (e.g., after a workflow has completed).

To run a workflow anytime a pull request event occurs, nest pull_request under on, like so:

(.github/workflows/pull-request.yml)

Under pull_request, nest types to specify the types of pull request events to trigger on and branches to narrow the pull requests to listen for events on by target branch.

The workflow should run for pull requests with a target branch of main and that have been created (created), reopened (reopened) or received a commit (synchronize).

(.github/workflows/pull-request.yml)

The workflow will run one job named pull-request (ID of the job). This job runs on a fresh virtual machine with the latest Ubuntu operating system installed.

(.github/workflows/pull-request.yml)

The pull-request job consists of several steps. The first step is to checkout the code with the checkout action, like so:

(.github/workflows/pull-request.yml)

This lets the workflow access your repository. Then, the job must install the latest LTS version of Node.js (16.x) on the virtual machine with the setup-node action, like so:

(.github/workflows/pull-request.yml)

Replace @GITHUBUSERNAME with your own GitHub username (must include the @ prefix). Once installed, the job installs the library's npm dependencies via the npm ci command, which works similarly to npm install, but is used in CI/CD environments to ensure a clean installation of the dependencies.

(.github/workflows/pull-request.yml)

Notice that the uses syntax applies to GitHub actions and the run syntax applies to single commands.

With the environment prepared, the job lints and tests the library's code, like so:

(.github/workflows/pull-request.yml)

Finally, the job uses the Jest Coverage Report action to generate a comment within the pull request that tabulates the statements, branches, functions and lines covered by tests.

(.github/workflows/pull-request.yml)

The action must be provided an action parameter of github-token to have permission to the repository.

Altogether...

(.github/workflows/pull-request.yml)

When you initialize a new GitHub repository and try to push the library to a new remote GitHub repository, you may run into the following error message:

This means your personal access token does not include the workflow scope. You should visit your GitHub tokens, select your personal access token (the one currently stored on your local machine) and check the workflow checkbox under the list of available scopes:

Note: If you have not set up a personal access token, then please set one up by following the directions in the official documentation here.

Now push to the main branch of the remote GitHub repository.

Under the "Actions" tab of the library's GitHub repository page, you will find the "Pull Request" workflow listed under "Workflows."

Suppose you add a new function to the library and decide to create a new pull request to have it reviewed before being merged to the main branch.

Once the pull request is created, GitHub triggers the "Pull Request" workflow.

Click "Details" to view the logs of the job as it's in progress.

Once the job finishes, revisit the pull request. You will notice that the checks have passed and a code coverage report has been generated by the Jest Coverage Report action.

Next Steps#

Workflow jobs in GitHub Actions can be composed from smaller actions. Visit GitHub's marketplace here to explore the many open-source actions that can be integrated into your CI/CD pipeline.

Try out GitHub Actions on your next project!

Sources#