Composite actions let you bundle multiple steps into a single, reusable action, simplifying complex workflows.
Let’s see one in action. Imagine you have a common task like building and pushing a Docker image. Instead of repeating the docker build and docker push commands in multiple workflow files, you can create a composite action.
Here’s a simple action.yml for a composite action named docker-build-push:
name: 'Docker Build and Push'
description: 'Builds and pushes a Docker image'
inputs:
image-name:
description: 'The name of the Docker image'
required: true
tag:
description: 'The tag for the Docker image'
required: true
dockerfile:
description: 'Path to the Dockerfile'
default: 'Dockerfile'
required: false
outputs:
digest:
description: 'The digest of the pushed image'
runs:
using: 'composite'
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ inputs.docker_username }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v4
with:
context: .
file: ${{ inputs.dockerfile }}
push: true
tags: ${{ inputs.image-name }}:${{ inputs.tag }}
outputs: type=registry
Now, in your workflow file, you can use this composite action like any other action:
name: CI
on:
push:
branches: [ main ]
jobs:
build_and_push:
runs-on: ubuntu-latest
steps:
- name: Build and push my app image
uses: ./actions/docker-build-push # Assuming your composite action is in a local 'actions/docker-build-push' directory
with:
image-name: 'my-docker-repo/my-app'
tag: '${{ github.sha }}'
dockerfile: 'Dockerfile.prod'
env:
docker_username: ${{ secrets.DOCKER_USERNAME }}
The real power of composite actions comes from abstracting away complexity. Instead of a workflow file looking like this:
jobs:
build_and_push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: 'Dockerfile.prod'
push: true
tags: 'my-docker-repo/my-app:${{ github.sha }}'
outputs: type=registry
It becomes this:
jobs:
build_and_push:
runs-on: ubuntu-latest
steps:
- name: Build and push my app image
uses: ./actions/docker-build-push
with:
image-name: 'my-docker-repo/my-app'
tag: '${{ github.sha }}'
dockerfile: 'Dockerfile.prod'
env:
docker_username: ${{ secrets.DOCKER_USERNAME }}
This makes workflows much cleaner and easier to read. You define the inputs and outputs in the action.yml file. Inputs are how you pass data into the action, and outputs are how the action can return data back to the workflow. In our example, image-name, tag, and dockerfile are inputs. The docker/build-push-action outputs a digest that we could capture if we needed it.
You can also define environment variables that are automatically set for all steps within the composite action. In the example, we pass docker_username as an environment variable, which is then used by the docker/login-action. This is a common pattern for securely passing credentials or configuration that the underlying steps need.
When you define runs: using: 'composite', you are telling GitHub Actions that this action is composed of a series of individual steps, just like you’d find in a regular workflow file. You can use any of the standard GitHub Actions features within these steps, including other actions (like actions/checkout@v3 or docker/setup-buildx-action@v2), shell commands, or even other composite actions.
A subtle but powerful aspect of composite actions is how they handle context and secrets. When you reference secrets.DOCKER_PASSWORD inside the composite action’s action.yml, it’s not directly accessing it. Instead, the workflow that calls the composite action provides that secret. The docker/login-action within the composite action then uses the password input provided by the calling workflow. This ensures that secrets are managed at the workflow level, not embedded within the action definition itself, which is a crucial security practice.
The outputs section in action.yml is key for making composite actions truly reusable and for enabling more complex workflow logic. For instance, if our docker-build-push action returned the image digest, a subsequent step in the workflow could use that digest to deploy a specific, immutable version of the image. This is achieved by assigning an id to the step that runs the composite action (e.g., id: push-image) and then referencing the output in a later step: echo "Pushed image digest: ${{ steps.push-image.outputs.digest }}".
The next logical step is to start publishing your composite actions to the GitHub Marketplace, making them available to other repositories or even the wider community.