Skip to main content
< Back to Blog

Automate Your Code with GitHub Actions #3: Jobs, Actions, and the Marketplace

Now that we understand the events and triggers that kick off our workflows, it's time to look at the building blocks that make up the actual work: jobs and actions.

In this article, we'll explore how to structure jobs, run them across multiple configurations with matrix strategies, and leverage the rich ecosystem of actions available in the GitHub Marketplace.

Who is Bas Steins?

Bas is a software developer and technology trainer based in the triangle between the Netherlands, Germany, and Belgium. For almost two decades, he has been helping people turn their ideas into software and helping other developers write better code. This series is based on his mini-guide "Automate Your Code with GitHub Actions".

Jobs

A job is a collection of steps that execute on the same runner. By default, jobs run in parallel, but you can configure them to run sequentially or conditionally based on the outcome of other jobs. Each job runs on a separate instance of the runner environment, which can be a GitHub-hosted runner or a self-hosted runner.

Syntax

jobs:
  <job_id>:
    runs-on: <runner>
    permissions:
      contents: <permission>
    needs: <job_id>
    id: <unique_id>
    if: <expression>
    environment: <environment>
    concurrency:
      group: <group>
    outputs:
        <output_id>: <output>
    env:
        <variable>: <value>

Attributes

  • runs-on: Specifies the type of runner environment for the job. The runner can be a GitHub-hosted runner (for example, ubuntu-latest, windows-latest, macos-latest) or a self-hosted runner.
  • permissions: Defines the permissions for the job. The contents key specifies the permission level for the job to access repository contents.
  • needs: Specifies the jobs that must complete successfully before the current job can start.
  • id: A unique identifier for the job. This is useful for referencing the job in other parts of the workflow.
  • if: Defines an expression that determines whether the job should run. The job runs only if the expression evaluates to true.
  • environment: Specifies the environment for the job. This can be used to define environment-specific settings or secrets.
  • concurrency: Defines the concurrency settings for the job. This can be used to limit the number of concurrent job runs.
  • outputs: Defines the outputs of the job that can be used in subsequent steps or jobs.
  • env: Specifies environment variables for the job.

Matrix Operations

There are use cases, in which you want to run a job or a step multiple times with different configurations. For example, you might want to test your code against different versions of a compiler, a library, or an operating system. Instead of duplicating the same job or step multiple times, you can use a matrix strategy to define a list of values that will be used to create a set of jobs or steps.

A matrix is defined as a dictionary with keys representing the variable names and values representing the list of values to iterate over. The matrix strategy is defined at the job level and can be used to create multiple jobs or steps based on the matrix configuration.

Mathematically speaking, you build a cartesian product of the values for each key in the matrix. For example, if you have a matrix with two keys os and compiler, and the values are ['ubuntu', 'windows'] and ['gcc', 'clang'] respectively, the resulting matrix will have four combinations: ['ubuntu', 'gcc'], ['ubuntu', 'clang'], ['windows', 'gcc'], and ['windows', 'clang']. In layman's terms, you will simply run the step with each possible combination of the values.

Here's an example of a matrix strategy in a job:

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        compiler: [gcc, clang]
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Build with ${{ matrix.os }} and ${{ matrix.compiler }}
        run: echo "Building with ${{ matrix.os }} and ${{ matrix.compiler }}"

As you can see, the strategy keyword is used to define the matrix configuration. In this case, we've two keys os and compiler with their respective values. The steps in the job will be executed for each combination of the values in the matrix. The ${{ matrix.os }} and ${{ matrix.compiler }} syntax is used to reference the values in the matrix.

Actions

Actions are the building blocks of a workflow. They are reusable units of code that can be combined to create a workflow. Actions can be used to automate any process in your repository, from building, testing, and deploying code to integrating with external services. As you've seen, each step consists of an action that performs a specific task.

Running Steps without Actions

You can run steps without actions by using the run keyword. This is useful for running shell commands or scripts directly in the workflow. Here's an example of a step that runs a shell command:

steps:
  - name: Run a shell command
    run: echo "Hello, World!"

In this step, the run keyword is used to execute the shell command echo "Hello, World!". Of course, you can run more complex commands or scripts in this way, but these need to be available in the environment where the workflow is running.

Available Shells

You can run shell commands in different shells, such as bash, sh, cmd, or powershell. The default shell is bash on Linux and macOS runners, and cmd on Windows runners. You can specify a different shell using the shell keyword. Here's an example of running a command in a different shell:

steps:
  - name: Run a command in a different shell
    run: echo "Hello, World!"
    shell: cmd

In this step, the shell keyword is used to specify the cmd shell on a Windows runner. You can also use bash, sh, or powershell as the value for the shell keyword.

More Complex Steps with Actions

Usually, you'll want to use actions to perform more complex tasks in your workflow. Actions are reusable units of code that can be combined to create a workflow. You can use actions provided by GitHub or third parties. In this chapter we'll cover some common actions and how to use them in your workflows.

Using the github-script Action

The github-script action allows you to run JavaScript code directly in your workflow. This action provides a way to interact with the GitHub API, access workflow data, and perform complex logic using JavaScript. Here's an example of using the github-script action:

steps:
  - name: Run JavaScript code
    uses: actions/github-script@v4
    with:
      script: |
        const { data } = await octokit.repos.get({ owner: 'octocat', repo: 'hello-world' });
        console.log(data);

Here are some common actions that you can use in your workflows:

  • checkout
  • setup-node
  • setup-python
  • setup-java
  • setup-dotnet
  • setup-go
  • setup-ruby
  • setup-elixir
  • setup-haskell
  • setup-erlang
  • setup-php
  • setup-rust
  • setup-perl

The Actions Marketplace

The Actions Marketplace is a central hub where you can discover, share, and reuse actions created by the community. It provides a wide range of actions for various use cases, from building and testing code to deploying applications and integrating with external services.

You can browse the marketplace to find actions that suit your needs, and easily integrate them into your workflows using the uses keyword.

In the next article, we'll learn how to create our own custom actions — including JavaScript actions, Docker actions, and composite actions. See you there!

Your Download is in Progress…

Giveaways. Cheat Sheets. eBooks. Discounts. And great content from our blog!