Architecting Reusable Automation: The Engineering of GitHub Actions Custom Actions

GitHub Actions has cemented its position as the dominant Continuous Integration and Continuous Deployment (CI/CD) platform for millions of developers. While the public marketplace offers thousands of pre-built actions that cover common deployment, testing, and setup tasks, there inevitably arrives a point where off-the-shelf solutions fall short. Building custom actions allows engineering teams to encapsulate complex logic, share functionality across multiple repositories, and create reusable automation that fits their specific operational needs perfectly. Whether the goal is to rotate authentication tokens securely, query internal APIs for deployment status, or standardize a complex build pipeline, custom actions provide the mechanism to achieve these objectives with precision.

The creation of a custom action is not merely a coding exercise; it is an architectural decision that impacts how workflows are structured, maintained, and shared. Developers must choose between three distinct implementation types: JavaScript actions, Docker container actions, and composite actions. Each type possesses unique strengths, dependencies, and use cases. Understanding these distinctions, along with the best practices for versioning, error handling, and publication, is essential for any engineer looking to extend the capabilities of their CI/CD infrastructure.

The Three Architectural Paradigms

Before diving into implementation details, it is critical to understand the structural differences between the three types of custom actions. The choice between JavaScript, Docker, and Composite actions dictates the environment in which the code runs, the startup speed, and the complexity of the dependencies.

  • JavaScript Actions run directly on the runner machine. They are executed within the existing Node.js environment provided by the GitHub Actions runner. This makes them exceptionally fast and lightweight, as there is no need to build or pull a container image. They offer direct access to the runner's file system and are cross-platform compatible, provided the code does not rely on OS-specific binaries.
  • Docker Container Actions package the code along with its entire environment, including all dependencies, libraries, and runtime configurations. This ensures a consistent execution environment regardless of the runner's base image. They are ideal for complex tasks that require specific software versions, heavy dependencies, or languages other than Node.js. However, they suffer from slower startup times due to the overhead of pulling and executing the container.
  • Composite Actions do not require any coding in a specific language. Instead, they serve as a wrapper that combines existing actions and shell commands into a single, reusable unit. They are designed for simplicity and reusability, allowing teams to chain together multiple steps (such as setting up a language, installing dependencies, and running a build) into one logical action.

Comparative Analysis of Action Types

Feature JavaScript Actions Docker Container Actions Composite Actions
Execution Environment Directly on the runner machine Inside a Docker container Directly on the runner machine
Startup Speed Fast Slower (due to image pull/execution) Fast
Language Support Node.js (JavaScript/TypeScript) Any language (via container) None (wrapper for existing actions)
Dependency Management npm packages Dockerfile layers Referenced existing actions
Primary Use Case Lightweight tasks, API interactions Complex dependencies, consistency Simple reusability, step aggregation

Real-World Implementation Scenarios

Custom actions shine when addressing specific, repetitive, or complex operational challenges that standard actions cannot handle elegantly. Two prominent examples illustrate the breadth of their utility: security token rotation and deployment status verification.

Automating Security Token Rotation with AWS and Artifactory

A practical example of custom action utility is the automation of security token rotation for artifact repositories. In one production scenario, a client needed to rotate their Artifactory token every 24 hours for security compliance. Artifactory serves as a centralized solution for managing artifacts, packages, files, and images within an organization. Access to these resources requires a username and a token, which must be rotated regularly to mitigate security risks.

The existing infrastructure was outdated, prompting a migration to GitHub Actions. Because the task required adjustments across multiple steps and integration with AWS services (specifically AWS CDK and IAM roles), a custom action was deemed the most efficient solution. The workflow utilized GitHub Actions' OIDC (OpenID Connect) capabilities to assume an AWS role without storing long-term credentials.

The pipeline structure for this custom action included:

  1. Triggering on Push: The workflow was configured to run on push events.
  2. AWS Credential Configuration: The aws-actions/configure-aws-credentials action was used to assume a specific IAM role (arn:aws:iam::${{ env.AWSAccountID}}:role/gha-role) in the eu-west-1 region. This leverages the id-token: write permission to interact with GitHub's OIDC provider.
  3. Custom Action Execution: The custom action, referenced via uses: ./, was invoked with specific inputs such as the username, the GitHub Actions token, and the secret name.

This approach demonstrated that custom actions can seamlessly integrate with external technologies like AWS and Node.js, providing a robust, automated solution for security-critical tasks.

yaml name: "This-is-a-test-pipeline" on: push: jobs: rotateaction: runs-on: ubuntu-latest permissions: id-token: write contents: read steps: - name: checkoutrepo uses: actions/checkout@v3 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@master with: role-to-assume: arn:aws:iam::${{ env.AWSAccountID}}:role/gha-role aws-region: eu-west-1 - name: UseOurAction uses: ./ with: username: "test_user" gha_token: ${{ gha_token }} secret_name: "TEST_SECRET"

Querying Deployment Status via GitHub API

Another common challenge arises when integrating with third-party hosting platforms like Vercel. In one case, a developer faced an issue where their blog was built twice on each push: once in the CI workflow for testing and once in Vercel for deployment. The goal was to run tests against the Vercel build to eliminate redundancy. However, although Vercel reported deployments correctly via the Deployments API, there was no direct way to access this information within the GitHub Actions build environment.

The solution was to build a custom action that queried GitHub's API to retrieve the desired deployment status. GitHub provides two API versions: v3 (REST) and v4 (GraphQL). Both support a wide range of fields and actions. The custom action utilized these APIs to fetch the deployment URL or status, allowing the CI workflow to test the actual deployed environment. This scenario highlights how custom actions can bridge gaps between different platforms and APIs, enabling more efficient and accurate testing pipelines.

Repository Structure and Versioning Strategy

Where and how a custom action is stored has significant implications for its maintainability and discoverability. If an action is intended for public or community use, it is strongly recommended to keep it in its own repository. This separation allows the action to be versioned, tracked, and released independently of application code. It narrows the scope of the codebase for contributors fixing issues or extending functionality and decouples the action's versioning from the application's release cycle.

For actions that are internal and not intended for external sharing, files can be stored in any location within the repository. However, the recommended convention is to store them in the .github directory, such as .github/actions/action-a or .github/actions/action-b. This keeps internal automation code organized and distinct from source code.

Referencing Actions in Workflows

The method for referencing an action in a workflow file depends on its location:

  • Local Actions: For actions within the same repository, reference them by their path. For example, ./.github/actions/setup-node-project.
  • External Actions: For actions in a separate repository, reference them using the owner/repo@version format. For example, your-org/security-scanner@v1.

```yaml

Example of referencing local actions

  • name: Setup project
    uses: ./.github/actions/setup-node-project
    with:
    node-version: '20'

  • name: Notify Slack
    if: always()
    uses: ./.github/actions/slack-notifier
    with:
    webhook-url: ${{ secrets.SLACK_WEBHOOK }}
    environment: production
    status: ${{ job.status }}
    ```

Versioning is critical for stability. Actions should use semantic versioning and git tags. Users must be able to pin to a specific version to avoid breaking changes. This practice ensures that workflows remain predictable and that updates to the action can be controlled and tested before adoption.

Best Practices for Robust Action Development

Building a custom action is only half the battle; ensuring it is reliable, user-friendly, and maintainable requires adherence to several best practices.

  • Single Responsibility Principle: Keep actions focused on a single task. It is better to have multiple small, specialized actions than one monolithic action that tries to do everything. This improves reusability and simplifies debugging.
  • Error Handling: Handle errors gracefully and provide meaningful error messages. In JavaScript actions, use core.setFailed() to properly signal failures to the workflow runner. This ensures that the workflow status reflects the outcome accurately and provides useful feedback to the developer.
  • Documentation: Document inputs and outputs thoroughly in the action.yml file. This metadata is crucial as it appears in the GitHub Marketplace and IDE integrations, helping users understand how to configure and use the action.
  • Testing: Test actions before publishing. Create a test workflow that exercises different input combinations and edge cases. This proactive testing catches issues before they impact production workflows.
  • Platform Compatibility: Many users access GitHub through GitHub Enterprise Server or custom domains. To ensure compatibility, avoid hard-coding references to API URLs like https://api.github.com. Instead, use environment variables such as GITHUB_API_URL for the REST API. This makes the action portable across different GitHub instances.

Publishing and Branding

Once an action is developed and tested, publishing it to the GitHub Marketplace expands its reach and usability. To share an action with the community, add branding information to the action.yml file. This includes specifying an icon (e.g., 'bell') and a color (e.g., 'blue'). After the branding is in place, create a release in the repository. GitHub will automatically prompt the user to publish the action to the marketplace if the action.yml file is properly configured. This process integrates the action into the broader ecosystem, making it discoverable by other developers and organizations.

Conclusion

Custom GitHub Actions represent a powerful extension of the platform's native capabilities, enabling developers to tailor their CI/CD pipelines to specific operational, security, and architectural needs. Whether encapsulating complex API queries, automating sensitive security rotations, or standardizing internal workflows, custom actions provide a structured, reusable, and maintainable solution. By choosing the appropriate action type—JavaScript for speed, Docker for consistency, or Composite for simplicity—and adhering to best practices in versioning, error handling, and documentation, engineering teams can build automation that scales effectively across their projects. The ability to decouple automation logic from application code and share it across repositories underscores the strategic value of custom actions in modern software development.

Sources

  1. GitHub Actions Custom Actions
  2. GHA: Lets create a custom action
  3. Manage custom actions
  4. Creating GitHub Actions

Related Posts