When working with GitHub Actions, composite actions are a great way to encapsulate and reuse common steps. However, they come with some limitations—one of which we recently encountered when using a third-party caching action that required a post step. In our setup, the caching action was failing because it was being used within a composite action, which doesn’t support post steps. In this blog post, we'll dive into the problem, explain why it happens, and explore several potential solutions along with code examples.
The Problem: Composite Actions and Missing Post Steps
What Are Composite Actions?
Composite actions let you group multiple steps into a single reusable action defined in YAML. They simplify workflows by bundling common logic that can be used across different repositories or workflows. However, unlike JavaScript or Docker container actions, composite actions lack a separate lifecycle that supports post steps (i.e., cleanup or finalization logic).
Our Specific Issue
We were using a third-party caching action in our GitHub Actions workflow. This caching solution required a post step to be executed after the main steps, ensuring that the cache state was properly updated at the end of the job. Unfortunately, when this caching action was called from within a composite action, GitHub Actions would ignore or fail the post step because composite actions don’t support them. The result was a broken caching mechanism.
Workarounds and Potential Solutions
1. Rearranging Workflow Steps
A straightforward workaround is to move the caching steps outside of the composite action and into the workflow file itself. This means you would explicitly include the caching step in your job definition, ensuring that the post step is executed properly.
For example, suppose you originally had a composite action that bundled checkout, caching, and node dependency setup. You could refactor your workflow as follows:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Restore Cache
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
- name: Setup Node Dependencies
run: npm ci
# Other steps such as running tests
- name: Run Tests
run: npm test
# Post step for caching (if required by the caching action)
- name: Finalize Cache
if: always()
run: echo "Running post steps for caching..."
By isolating the caching step and its corresponding post logic in the workflow file, you bypass the composite action’s limitation.
2. Using a Different Action Type
If encapsulating the caching logic (including the post step) within a single reusable unit is critical for your project, consider using a JavaScript or Docker container action instead of a composite action. These action types allow you to define both a main execution phase and a post step for cleanup or finalization.
For example, a JavaScript action might look like this:
# action.yml for a JavaScript Action
name: 'My Caching Action'
description: 'An action that performs caching with a post step'
runs:
using: 'node12'
main: 'dist/index.js'
post: 'dist/post.js'
Here, dist/index.js contains your primary logic, and dist/post.js contains the logic that should run after the main steps, even if an error occurred.
3. Using YAML Anchors for DRY Workflows
If the workaround in option 1 leads to repetition across multiple workflows or jobs, you can reduce redundancy using YAML anchors. Note: YAML anchors work only within the same file; they cannot be referenced from a different file.
Example Using YAML Anchors
# .github/workflows/ci.yml
# Define common steps once using YAML anchors
steps_common: &common_steps
- name: Checkout Repository
uses: actions/checkout@v2
- name: Restore Cache
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
- name: Setup Node Dependencies
run: npm ci
jobs:
build:
runs-on: ubuntu-latest
steps:
<<: *common_steps
- name: Run Tests
run: npm test
- name: Finalize Cache
if: always()
run: echo "Running post steps for caching..."
This approach ensures that you maintain a single source of truth for these common steps, making future updates easier.
4. Reusable Workflows
Another alternative is to use reusable workflows which let you encapsulate a series of steps in one workflow and call it from others. This method provides a centralized location for managing common logic across multiple workflows without duplicating code.
Example of a Reusable Workflow
Reusable Workflow (.github/workflows/reusable-steps.yml):
name: Reusable Steps
on:
workflow_call:
inputs:
cache-key:
required: true
type: string
jobs:
common:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Restore Cache
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ inputs.cache-key }}
- name: Setup Node Dependencies
run: npm ci
Calling Workflow (.github/workflows/ci.yml):
name: CI Pipeline
on:
push:
branches:
- main
jobs:
build:
uses: ./.github/workflows/reusable-steps.yml
with:
cache-key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
post:
runs-on: ubuntu-latest
needs: build
steps:
- name: Finalize Cache
if: always()
run: echo "Running post steps for caching..."
This setup centralizes your common steps and still allows you to add job-specific post steps outside the reusable workflow.
Composite actions in GitHub Actions provide a powerful way to reuse common logic, but they come with the caveat that they don’t support post steps. This limitation can cause issues when using third-party actions—such as caching actions—that rely on post steps for cleanup.
To work around this limitation, you have several options:
- Rearrange your workflow steps so that caching and its associated post step are defined directly in the workflow.
- Switch to JavaScript or Docker container actions if you need the encapsulation of a single action with post steps.
- Utilize YAML anchors within a workflow file to avoid code duplication.
- Adopt reusable workflows to centralize common logic across multiple workflows.
By choosing the approach that best fits your project's needs, you can maintain a clean, maintainable CI/CD pipeline while working around the current limitations of composite actions.
Happy coding!