GitHub Actions provides a robust infrastructure for continuous integration and deployment, but as organizations scale, the need for modularity becomes critical. Developers face a distinct choice when abstracting common logic: Composite Actions or Reusable Workflows. While Reusable Workflows allow for high-level workflow orchestration without requiring a repository checkout, Composite Actions serve a different, more granular purpose. They are designed to encapsulate a sequence of individual steps into a singular, reusable entity. This approach enhances the modularity and efficiency of workflows by allowing teams to bundle multiple commands or even other actions into a single, cohesive unit. However, this encapsulation comes with specific mechanical requirements and architectural consequences that engineering leaders must understand before widespread adoption.
Structural Mechanics and Implementation
The fundamental building block of a Composite Action is the action.yml file. By convention, this file is placed at the root of a repository, but this is not a strict mandate. Teams can structure their repositories to contain multiple composite actions by placing them in separate directories, each containing its own action.yml file. This flexibility allows for organized grouping of related actions, such as separating database utilities from deployment scripts within the same repository.
The action.yml file begins with metadata that defines the identity and purpose of the action. For a database migration utility, this might look like:
yaml
name: "Database Migration"
description: "Migrate a Postgres service spinned up \
for testing purposes."
Following the metadata, the action requires the definition of inputs. These inputs allow the composite action to receive data from the calling workflow, ensuring that the bundled steps are dynamic rather than static. The core of the composite action is defined by the runs key, which signals to GitHub Actions that the action is not a traditional Docker container or JavaScript-based action, but rather a composite of multiple steps.
A critical technical detail in the runs definition is the using field, which is set to composite. This distinction is crucial because it dictates how the action is executed. Within the steps array, individual commands are defined. For example, if the action needs to execute shell commands, the shell field must be explicitly set. GitHub Actions supports various shells, including bash, sh, pwsh (PowerShell), and python. The choice of shell determines the syntax and features available for the commands. If the shell is set to bash, all commands specified in the run fields are executed in a Bash environment.
yaml
runs:
using: "composite"
steps:
- name: Run Migration
shell: bash
run: |
echo "Running migration..."
With this structure, a developer has successfully defined a composite action that can be reused across multiple workflows. For instance, a database migration composite action ensures consistent migration processes by bundling the necessary SQL execution steps, environment variable setup, and verification logic into a single, reusable unit.
Publishing and Versioning Strategies
Once the action.yml file is finalized, the action must be published to GitHub to make it available for use in workflows. The standard process involves committing the changes and pushing them to the repository:
bash
git add .
git commit -m "Publish composite action"
git push origin main
For better management and to facilitate controlled updates, it is essential to tag the action with a version, such as v1 or v2. This versioning approach ensures that workflows reference specific, stable versions of the action, allowing for compatibility management and preventing breaking changes from inadvertently affecting production pipelines.
bash
git tag -a v1 -m "Initial release of db migration action"
git push origin v1
When referencing a composite action in a workflow, the syntax follows the format {owner}/{repo}[@{ref}]. For example, Ikeh-Akinyemi/composite-github-action@v1 points to version one of the composite action in the specified repository. Instead of a version tag, developers can also use a specific commit SHA, such as Ikeh-Akinyemi/composite-github-action@4a3ddaf9b2914638ca2be9f4b21af5d01d9d3e22, or a branch name like Ikeh-Akinyemi/composite-github-action@main.
However, versioning introduces a significant nuance when managing multiple composite actions within a single repository. Because git tags apply to the entire repository, updating one composite action and tagging a new release applies that release number to all composite actions in the repository, even if others have not changed. This is a critical consideration for teams maintaining a monorepo structure for their actions. Furthermore, if a team intends to publish an action on the GitHub Marketplace, the repository must contain only a single action.
The Observability and UI Limitations
While Composite Actions offer excellent code reuse and modularity, they introduce significant challenges regarding visibility and monitoring. When a Composite Action is used as a step in an existing workflow, it is collapsed into a single entry in the GitHub Actions user interface.
yaml
jobs:
my_job:
name: "Job 1"
runs-on: ubuntu-latest
steps:
- name: Step 1
uses: ./.github/composite-actions/some-series-of-steps
This abstraction means that all the internal steps of the composite action are hidden behind the "Step 1" label. This is a long-standing issue within the GitHub Actions platform. Consequently, users do not get log collapsing, bubbled-up timing, or granular visibility for individual steps within the composite action. This lack of granularity can make debugging difficult, as the root cause of a failure might be obscured within the aggregated logs of the single composite step.
The implications extend beyond the GitHub UI to third-party observability tools. Teams using integrations like Datadog to collect CI information face similar limitations. Datadog integrates with GitHub Actions to analyze pipeline executions, breaking them down by job duration and individual steps. However, because a Composite Action is inlined into a single step, Datadog receives only a single entry for the entire composite action. This prevents engineers from drilling down into the specific internal steps to identify performance bottlenecks or slow bits within the abstracted logic.
Composite Actions vs. Reusable Workflows
When managing multiple projects with similar deployment patterns, repeating the same GitHub Actions steps can become tedious, error-prone, and hard to maintain. GitHub Actions provides two primary solutions for standardizing and scaling CI/CD pipelines: Composite Actions and Reusable Workflows. Understanding the distinction between these two is vital for designing an effective DevOps strategy.
Reusable Workflows, defined by the workflow_call trigger, allow an entire workflow to be called from another workflow. They offer a higher level of abstraction and can function without a repository checkout. In contrast, Composite Actions are essentially bundles of steps that require a repository checkout for utilization. Reusable Workflows cannot call and consume other reusable workflows directly, whereas Composite Actions allow for the bundling of multiple workflow steps into a single action.
The choice between the two often depends on the team's priorities regarding UI visibility and monitoring. Some teams have switched from Composite Actions to Reusable Workflows specifically to regain visibility in their CI/CD pipelines. By using a workflow with workflow_call, teams can achieve a better UI in GitHub Actions, with a clearer breakdown of steps. This shift allows for granular tracking in observability tools like Datadog, enabling teams to analyze and optimize the slow bits of their deployment processes. In cases where the primary goal is standardization without the need for deep internal step visibility, Composite Actions remain a potent tool. However, for teams requiring detailed telemetry and UI clarity, the trade-offs of Composite Actions may outweigh their benefits.
Conclusion
Composite Actions represent a powerful mechanism for achieving modularity within GitHub Actions, allowing developers to encapsulate complex sequences of steps into reusable, maintainable units. They are particularly useful for enforcing consistency in tasks such as database migrations, dependency installation, and Docker image building. However, their utility is counterbalanced by significant limitations in observability and user interface clarity. The collapsing of internal steps into a single UI entry can hinder debugging and prevent granular performance analysis in third-party monitoring tools.
As organizations continue to refine their CI/CD pipelines, the decision to use Composite Actions should be weighed against the need for visibility. For teams prioritizing strict internal abstraction and where the internal steps are simple or well-understood, Composite Actions offer a clean, DRY (Don't Repeat Yourself) approach. Conversely, for teams requiring deep insights into pipeline performance and detailed error reporting, Reusable Workflows may offer a superior alternative. Ultimately, the most effective CI/CD strategy often involves a hybrid approach, leveraging Composite Actions for small, atomic operations and Reusable Workflows for broader, observable pipeline orchestration.