GitHub Actions can run code on your behalf, but the truly mind-bending part is that anyone can trigger code to run on your GitHub repository, and you can define that code to be anything from a simple script to a full-blown CI/CD pipeline.
Let’s see it in action. Imagine you want to automatically test your Python code whenever you push a change. Here’s a workflow file, .github/workflows/python-app.yml, that does just that:
name: Python application
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 1.20
uses: actions/setup-python@v3
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint with flake8
run: |
pip install flake8
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Test with pytest
run: |
pip install pytest
pytest
This YAML file defines a workflow named "Python application". The on: section specifies that this workflow will trigger on push events to the main branch and pull_request events targeting the main branch.
The core of a workflow is its jobs. Here, we have a single job called build. This job is configured to run on runs-on: ubuntu-latest, meaning it will execute on a fresh Ubuntu Linux virtual machine provided by GitHub.
Inside a job are steps. Each step is an individual task.
- The first step,
uses: actions/checkout@v3, is a pre-built action that checks out your repository’s code so the workflow can access it. - The second step,
name: Set up Python 1.20, uses another pre-built action,actions/setup-python@v3, to install a specific Python version (3.10 in this case) on the runner. Thewith:block passes configuration to this action. - The next two steps execute shell commands using
run:. The first installs dependencies by upgrading pip and then installing fromrequirements.txt. The second installsflake8and runs it for linting. - The final step also uses
run:to installpytestand execute your tests.
The problem this solves is automating repetitive tasks associated with software development, particularly testing and deployment. Instead of manually cloning repositories, setting up environments, and running commands, you declaratively define these actions in a YAML file. GitHub then orchestrates the execution of these steps on dedicated virtual machines (runners) whenever the specified events occur.
Internally, GitHub Actions runners are virtual machines that are provisioned for each job. When a workflow runs, GitHub spins up a runner, downloads your code, and executes each step sequentially. Actions are essentially reusable units of code that encapsulate common tasks, making workflows cleaner and more modular. You can use pre-built actions from the GitHub Marketplace or create your own.
The runs-on directive is crucial for controlling the execution environment. You can choose from various GitHub-hosted runners (like ubuntu-latest, windows-latest, macos-latest) or even host your own self-hosted runners for more control over the environment and hardware.
The secrets context is where you store sensitive information like API tokens or SSH keys. You can define these in your repository’s or organization’s settings, and then reference them in your workflow using ${{ secrets.MY_SECRET_NAME }}. This ensures that sensitive data is not exposed in your workflow logs.
One of the most powerful, yet often overlooked, aspects of GitHub Actions is the ability to create composite actions. These allow you to bundle multiple steps into a single reusable action defined within your repository. You can then call this composite action from other workflows, promoting DRY (Don’t Repeat Yourself) principles and making complex workflows much more manageable.
The next logical step is to explore how to manage dependencies and artifacts across different jobs within a workflow.