The Swiss Army Knife for the Agency Project, Part 1: GitHub Actions

Our research project has been using GitHub tools now for years. In addition to code hosting, GitHub offers vast capabilities for continuous integration, dependency updates, release management, artifact distribution, project organization, and community communications. In this article series, I summarize our team’s different experiments regarding GitHub features.

Our research project has been utilizing GitHub tools for several years. Most of the features are free for publicly hosted open-source projects. We have used these tools with an experimental spirit, gathering experience from the different features. Although our project does not aim directly at production, we take a production-like approach to many things we do.

In this article series, I summarize our team’s different experiments regarding GitHub features. As GitHub’s functionality is easily extensible through its API and abilities for building custom GitHub actions, I also explain how we have built custom solutions for some needs.

The series consists of three articles:

  1. This first part overviews GitHub Actions as a continuous integration platform.
  2. The second part concentrates on release management and software distribution.
  3. Finally, in the last post, we introduce other useful integrations and features we used during our journey.

CI Vets the Pull Requests

GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform for building automatic workflows. One defines the workflows as YAML configurations, which get stored along with repository code in a dedicated subfolder (.github/workflows). The GitHub-hosted platform automatically picks up and executes these workflows when defined trigger events happen.

A workflow can contain multiple jobs. Each workflow job consists of different steps. The logs from the steps can be browsed in the Actions UI.

Snippet below shows how the “test” job is configured:

name: test
on:
  # run whenever there is a push to any branch
  push:
jobs:
  test:
    # runner machine
    runs-on: ubuntu-latest
    steps:

      # check out the repository code
      - uses: actions/checkout@v4

      # setup node environment
      - uses: actions/setup-node@v4
        with:
          node-version: '18.x'

      # install dependencies, build, test
      - name: install deps
        run: npm ci
      - name: build
        run: npm run build
      - name: test
        run: npm test

      # upload test coverage to external service
      - name: upload coverage
        uses: codecov/codecov-action@v4
        with:
          files: ./coverage/coverage-final.json
          fail_ci_if_error: true
          token: ${{ secrets.CODECOV_TOKEN }}

In our project development model, developers always introduce changes through a pull request. CI runs a collection of required checks for the pull request branch, and only when the checks succeed can the changes be integrated into the main development branch.

CI checks are visible in the pull request view.

Our code repositories typically have a dedicated workflow for running linting, unit testing, license scanning, and e2e tests. CI runs these workflow jobs whenever the developer pushes changes to the PR branch. The CI jobs are visible in the PR view, and merging is impossible if some required checks fail.

Actions Simplify the Workflows

The basic structure of a GitHub Actions job definition contains:

  • Setting up the base environment for the workflow.
  • Checking out the repository code.
  • Execution steps that one can define as command-line scripts or actions.

Actions are reusable components written to simplify workflows. GitHub offers actions for the most commonly needed functionalities. You can also create custom actions specific to your needs or search for the required functionality in the actions marketplace, where people can share the actions they have made.

Example of using a custom action in a workflow:

name: test
on: push
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: setup go, lint and scan
        uses: findy-network/setup-go-action@master
        with:
          linter-config-path: .golangci.yml

For instance, we have written a custom action for our Go code repositories. These repositories need similar routines to set up the Go environment, lint the code, and check its dependencies for incompatible software licenses. We have combined this functionality into one custom action we use in each repository. This way, the workflow definition is kept more compact, and implementing changes to these routines is more straightforward as we need to edit only the shared action instead of all the workflows.

Custom action configuration example:

runs:
  using: "composite"
  steps:

    # setup go environment
    - uses: actions/setup-go@v5
      if: ${{ inputs.go-version == 'mod' }}
      with:
        go-version-file: './go.mod'
    - uses: actions/setup-go@v5
      if: ${{ inputs.go-version != 'mod' }}
      with:
        go-version: ${{ inputs.go-version }}

    # run linter
    - name: golangci-lint
      if: ${{ inputs.skip-lint == 'false' }}
      uses: golangci/golangci-lint-action@v4
      with:
        version: ${{ inputs.linter-version }}
        args: --config=${{ inputs.linter-config-path }} --timeout=5m

    # run license scan
    - name: scan licenses
      shell: bash
      if: ${{ inputs.skip-scan == 'false' }}
      run: ${{ github.action_path }}/scanner/scan.sh

In this shared action, setup-go-action, we can run our custom scripts or use existing actions. For example, we set up the Go environment with actions/setup-go, which is part of the GitHub offering. Then, we lint the code using the golangci/golangci-lint action, which is made available by the golangci-team. Lastly, the action utilizes our script to scan the licenses. We can efficiently combine and utilize functionality crafted by others and ourselves.

Runners Orchestrating E2E-tests

GitHub Actions workflow view run shows the jobs and their hierarchy. The artifacts and logs are available for each job also in this view.

In addition to linting, scanning, and unit tests, we typically run at least one e2e-styled test in parallel. In e2e tests, we simulate the actual end-user environment where the software will be run and execute tests with bash scripts or test frameworks that allow us to run tests through the browser.

Workflows can contain multiple jobs that run in parallel or sequentially. In this example, we have utilized the matrix strategy to run the same job with different parameters.

In these e2e tests, we typically set up the test environment using Docker containers, and the GitHub-hosted runners have worked well for orchestrating the containers. We can define needed container services in the workflow YAML configuration or use, e.g., Docker’s compose tool directly to launch the required containers.

Example of defining a needed database container in a GitHub Actions workflow:

    services:
      postgres:
        image: postgres:13.13-alpine
        ports:
          - 5433:5432
        env:
          POSTGRES_PASSWORD: password
          POSTGRES_DB: vault

External Integrations

Once the CI has run the tests, we use an external service to analyze the test coverage data. Codecov.io service stores the coverage data for the test runs. It can then use the stored baseline data to compare the test run results of the specific pull request. Once the comparison is ready, it gives feedback on the changes’ effect on the coverage. The report gets visualized in the pull request as a comment and line-by-line coverage in the affected code lines.

Codecov.io inserts the coverage report as a comment to the pull request view through the GitHub API.

Codecov is an excellent example of integrating external services into GitHub Actions workflows. Each integration needs a GitHub application that gives the third-party tool access to your organization’s resources. The organization must review the required permissions and install the application before GitHub allows the integration to work. One can also build GitHub applications for custom purposes. Read more about how a GitHub application can be helpful when automating release versioning.

Next Up: Release Management

This article gave an overview of how Findy Agency uses GitHub Actions as a continuous integration platform. The next one will be about release management. Stay tuned.