Mastering GitHub Actions: Building Composite Actions

In the previous lessons of our Mastering GitHub Actions series, we explored how to create workflows and use marketplace actions. However, as your CI/CD pipelines grow, you will often find yourself repeating the same sequences of steps across different repositories or workflows. This violates the DRY (Don't Repeat Yourself) principle. This is where Composite Actions come into play.

What are Composite Actions?

A Composite Action allows you to bundle multiple workflow steps into a single action. Think of it as creating a custom function in programming. Instead of writing ten lines of code to set up a Java environment, configure caching, and run a build in every workflow file, you can wrap those steps into one reusable action.

Composite Actions are defined in a file named action.yml or action.yaml. Unlike standard workflows, they use a specific syntax that tells GitHub to execute multiple shell commands or other actions as a single unit.

Why Use Composite Actions?

  • Code Reusability: Write once, use in multiple workflows.
  • Maintainability: Update the logic in one place instead of hunting through dozens of YAML files.
  • Readability: Keep your main workflow files clean and high-level.
  • Abstraction: Hide complex shell scripts or setup logic from the end-user.

The Structure of a Composite Action

To build a composite action, you must create a directory (usually in .github/actions/your-action-name) and add an action.yml file. The core structure looks like this:

name: 'My Composite Action'
description: 'Greets the user and installs dependencies'
inputs:
  user-name:
    description: 'Who to greet'
    required: true
    default: 'Developer'
runs:
  using: "composite"
  steps:
    - name: Greet User
      run: echo "Hello ${{ inputs.user-name }}"
      shell: bash
    - name: Check Version
      run: java -version
      shell: bash
    

The Flow of a Composite Action

[ Main Workflow ] 
       |
       v
[ Call Composite Action ] ----> [ Input Variables ]
       |                               |
       +-------------------------------+
       |
       v
[ Step 1: Run Shell Script ]
       |
       v
[ Step 2: Run Another Action ]
       |
       v
[ Step 3: Set Output ]
       |
       v
[ Return to Main Workflow ]
    

Step-by-Step Example: Java Build Composite Action

Let's create a practical example for a Java developer. This action will set up the JDK, cache Maven dependencies, and compile the project.

1. Create the Directory

In your repository, create the folder path: .github/actions/java-setup-build/.

2. Create the action.yml

name: 'Java Setup and Build'
description: 'Sets up JDK 17 and runs Maven compile'
inputs:
  java-version:
    description: 'Version of Java to use'
    default: '17'
runs:
  using: "composite"
  steps:
    - name: Set up JDK
      uses: actions/setup-java@v3
      with:
        java-version: ${{ inputs.java-version }}
        distribution: 'temurin'
        cache: 'maven'

    - name: Build with Maven
      run: mvn clean compile
      shell: bash
    

3. Use it in your Workflow

Now, in your main .github/workflows/main.yml, you can call it like this:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v3

      - name: Run My Composite Action
        uses: ./.github/actions/java-setup-build
        with:
          java-version: '21'
    

Real-World Use Cases

  • Environment Setup: Bundling the installation of CLI tools (like AWS CLI, Terraform, or Kubernetes tools) and authenticating them.
  • Monorepo Management: Creating specific build actions for different microservices within the same repository.
  • Security Scanning: Wrapping multiple security tools (SAST, DAST, dependency check) into a single "Security Gate" action.
  • Deployment Logic: Standardizing how applications are pushed to staging or production environments.

Common Mistakes to Avoid

  • Missing the Shell Attribute: In composite actions, every run step must have a shell attribute (e.g., shell: bash). If omitted, the action will fail.
  • Hardcoding Values: Avoid hardcoding versions or paths. Use inputs to make the action flexible.
  • Relative Path Confusion: Remember that composite actions run relative to the workspace of the calling workflow. Always test your paths.
  • Overcomplicating: If an action becomes too large, consider breaking it into smaller composite actions or a reusable workflow.

Interview Notes: Composite Actions vs. Reusable Workflows

This is a common interview question. Here is a quick breakdown:

  • Composite Actions: Used for grouping steps. They are faster to call and feel like a single step in the UI. They cannot have their own "secrets" directly passed as environment variables easily without inputs.
  • Reusable Workflows: Used for grouping jobs. They allow you to run jobs on different runners and provide better visualization in the GitHub Actions UI for complex pipelines.
  • Key Tip: Use Composite Actions for small, repetitive tasks. Use Reusable Workflows for entire deployment or testing pipelines.

Summary

Building Composite Actions is a vital skill for any DevOps engineer or Java developer using GitHub Actions. It allows you to create clean, modular, and maintainable CI/CD pipelines. By defining an action.yml with using: "composite", you can encapsulate complex logic and share it across your organization.

In the next lesson, we will dive deeper into Action Metadata and Versioning to learn how to share your custom actions with the wider world via the GitHub Marketplace.