Solving Post Step Issues in GitHub Composite Actions

In this latest blog from the developers on our Product Traction team, they share their experience using GitHub composite actions in a case study involving third-party caching.

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!


The Thin Air Labs Product Traction team provides strategic product, design and development services for companies of all sizes, with a specific focus on team extensions where they seamlessly integrate into an existing team. Whether they are deployed as a team extension or as an independent unit, they always work with a Founder-First Mindset to ensure their clients receive the support they need.

To learn more about our Product Traction service, go here.

Build what's next with us