GitHub Actions workflows are YAML files that define a series of jobs, each composed of steps, to automate tasks within your repository.
Here’s a workflow that builds and tests a Node.js project:
name: Node.js CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: '16.x'
- run: npm ci
- run: npm test
This workflow triggers on pushes and pull requests to the main branch. It checks out the repository code, sets up Node.js version 16.x, installs dependencies using npm ci (which is generally faster and more reliable for CI environments than npm install), and then runs the test suite.
The core of a workflow is the jobs section. Each job runs in an isolated environment defined by runs-on. Common runners include ubuntu-latest, windows-latest, and macos-latest. Jobs can depend on each other using needs, ensuring a specific order of execution.
Inside a job, steps are executed sequentially. A step can be a run command, which executes a shell script, or a uses action. Actions are reusable units of code that can be shared from the GitHub Marketplace or your own repository. actions/checkout@v3 is a fundamental action that fetches your repository’s code, making it available to subsequent steps. actions/setup-node@v3 is another common action that configures a specific Node.js version.
The on keyword defines the events that trigger a workflow. This can include push, pull_request, schedule (for cron-like triggers), workflow_dispatch (for manual triggers), and many more. You can be very specific about which branches, tags, or even file changes trigger a workflow.
You can pass context and secrets into your workflow. github is a predefined context that provides information about the event that triggered the workflow, the repository, and the actor. Secrets, like API tokens, are stored in your repository’s settings and accessed using the secrets context (e.g., ${{ secrets.MY_API_KEY }}).
When a job fails, GitHub Actions will stop the workflow by default. You can control this behavior with continue-on-error: true on a specific step or job, allowing subsequent steps or jobs to run even if a preceding one fails. This is useful for tasks like linting or static analysis where you might want to report issues without blocking a build.
The if conditional allows you to control whether a step or job runs based on expressions. These expressions can evaluate context variables, secrets, outputs from previous steps, or even results of shell commands. For example, if: github.ref == 'refs/heads/main' would only run a step on the main branch.
Consider the matrix strategy for building and testing across multiple configurations. For instance, you could test your application against different Node.js versions and operating systems without duplicating your job definition:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
This matrix strategy will automatically create separate jobs for each combination defined in node-version, running the same steps with different Node.js versions.
One subtle but powerful aspect of workflow syntax is the concept of outputs. A step can define an output using echo "::set-output name=output_name::output_value". This output can then be accessed by subsequent steps or jobs using steps.<step_id>.outputs.<output_name>. This allows for dynamic data passing between different parts of your workflow, enabling complex orchestration.
The workflow_dispatch event is incredibly useful for manually triggering workflows, especially when you want to pass parameters. You can define inputs for your workflow, allowing users to specify values when triggering it manually from the GitHub UI. This is perfect for tasks like deploying to a specific environment or running a particular test suite.
The permissions keyword at the workflow or job level allows you to explicitly grant or deny access to specific API scopes for the GITHUB_TOKEN. By default, the GITHUB_TOKEN has broad permissions. Restricting these permissions to only what your workflow needs is a crucial security best practice.
The next concept to explore is the use of reusable workflows, which allow you to share common job logic across multiple repositories or even within different workflows in the same repository.