Or press ESC to close.

Efficient Cross-Repository CI/CD: Running Targeted Tests with GitHub Actions

Sep 28th 2023 15 min read
  • medium
    github
    ci/cd
    shell

    In the dynamic realm of modern software development, Continuous Integration (CI) and Continuous Delivery (CD) have evolved from mere practices into essential cornerstones of efficient software delivery. The CI/CD pipeline fosters a seamless flow from code creation to deployment, ensuring rapid iteration and high-quality releases. However, as development ecosystems expand and projects modularize into separate repositories, a new challenge emerges: how to orchestrate targeted tests across distinct repositories in response to changes made in another.

    This is where the power of GitHub Actions, a robust automation and workflow automation tool, comes into play. The hallmark of a robust CI/CD setup lies not just in the ability to execute tests but in the capacity to selectively trigger tests in specific repositories in response to changes elsewhere. This is especially crucial when dealing with complex applications split into separate modules - such as mobile and web components - residing in different repositories.

    In this article, we delve into the intricacies of this challenge and present an elegant solution. We'll explore the intricacies of orchestrating targeted test runs across independent repositories and unveil the capabilities of GitHub Actions as the linchpin of this cross-repository CI/CD strategy.

    Setting the Stage

    In the constantly evolving realm of software development, intricate architectures frequently require solutions that go beyond conventional paradigms. Consider a scenario where a complex web application is divided into two distinct repositories: one dedicated to the user interface (UI) and the other to the API. Alongside these repositories, a dedicated test automation repository houses modules designed for UI and API testing, respectively. While this modular configuration offers increased flexibility and efficiency, it also necessitates the establishment of a seamless Continuous Integration and Continuous Delivery (CI/CD) workflow that spans these autonomous repositories.

    At the core of this scenario are the web application's fundamental components - the UI and the API - each residing in its dedicated repository. This separation empowers developers to focus on their specific areas of expertise, promoting testing scalability. However, with the repositories standing independently, a crucial challenge emerges: How can you efficiently execute tests that inherently rely on one another and reside in separate repositories?

    The conventional approach to CI/CD often involves running a comprehensive battery of tests across the entire application. In a multi-repository setup like this one, running the full suite of tests for every minor change becomes resource-intensive and time-consuming. The need for targeted test execution becomes evident - to minimize testing overhead while ensuring thorough testing of essential application components. This precision not only optimizes resources but also accelerates feedback loops, expediting the development cycle.

    The presence of two repositories, one for the UI and another for the API, introduces synchronization complexities. Effectively coordinating changes between these repositories and their corresponding test automation repository demands a sophisticated orchestration mechanism. Developers must have confidence that changes made in one repository will trigger the relevant tests in the other repository. To achieve this synchronization, a solution is required that can overcome the compartmentalization of these independent modules and facilitate targeted test runs based on contextual changes.

    In light of these challenges, it becomes increasingly evident that establishing an efficient CI/CD pipeline across separate repositories isn't just a preference; it's a necessity. The ability to meticulously select and trigger specific tests based on the repository undergoing modifications can redefine the testing landscape, enhancing the accuracy of test coverage and expediting the development process. In the forthcoming sections, we will explore how the ingenious integration of GitHub Actions serves as the catalyst to seamlessly bridge these repositories, enabling the orchestration of targeted tests that underpin a holistic CI/CD strategy.

    Configuring GitHub Actions

    In this section, we'll dive into the nitty-gritty of configuring GitHub Actions to orchestrate targeted tests across our separate repositories. Proper configuration is the linchpin of this cross-repository CI/CD strategy, allowing us to trigger specific tests based on contextual changes.

    Before we delve into discussing the setup, allow me to take a moment to describe the automation project utilized for testing purposes. This description will facilitate a better understanding of the remainder of the blog post. Our automation project is a straightforward NodeJS project that uses WebdriverIO. It has two spec files:

    In the package.json file we added two scripts for running our tests:

                                           
    "ui-tests": "npx wdio run ./wdio.conf.js --spec ui.spec.js",
    "api-tests": "npx wdio run ./wdio.conf.js --spec api.spec.js"
                          

    And that's it. Now we can run our tests from our automation repository with npm run ui-tests or npm run api-tests. Back to the GitHub Actions...

    Workflow Files

    In the realm of GitHub Actions, workflow files are the architects of automation. They define the sequence of steps that GitHub should execute when specific events occur in our repositories. Understanding the structure of these workflow files is pivotal in configuring GitHub Actions effectively for orchestrating targeted tests.

    A workflow file is essentially a YAML document that resides in the .github/workflows directory of our repository. It carries a file extension of .yml or .yaml. Here's an overview of its key elements:

    Name: The name field allows us to assign a descriptive name to our workflow. This name is displayed in the Actions tab of our repository, making it easy to identify the workflow.

                                           
    name: Trigger API Tests
                          

    On: The on field specifies the events that trigger our workflow. We can define various event types, such as push (when code is pushed to the repository) or custom events like repository_dispatch (which we use to trigger tests in this setup). In our case, we've set up custom events for UI and API testing, such as execute-ui-tests and execute-api-tests.

                                           
    on:
    repository_dispatch:
        types:
        - execute-ui-tests
        - execute-api-tests
                          

    Jobs: Under the jobs section, we define one or more jobs that run in parallel or sequentially. Each job represents a set of steps to be executed. We can name our jobs to reflect their purpose.

                                           
    jobs:
        trigger-api-tests:
                          

    Runs-On: Within a job, the runs-on field specifies the type of virtual machine or environment in which the job runs. GitHub Actions offers various environments, such as ubuntu-latest for Ubuntu-based runners and windows-latest for Windows-based runners.

                                           
    runs-on: windows-latest
                          

    Steps: The steps section contains an array of individual steps that GitHub Actions executes within the job. Each step performs a specific task, such as checking out code, setting up dependencies, or running tests.

                                           
    steps:
        - name: Checkout automation repository
            uses: actions/checkout@v4
            with:
                token: ${{ secrets.PAT }}
                          

    In this subsection, we've highlighted the fundamental elements of a GitHub Actions workflow file. These files serve as blueprints for our automation, allowing us to define precisely how our CI/CD processes should behave in response to various events. Understanding and customizing these files is key to configuring GitHub Actions to fit our specific development and testing needs.

    Event Triggers

    Event triggers lie at the heart of GitHub Actions, determining when our workflows should be initiated. In our cross-repository CI/CD setup, we've customized these event triggers to ensure that the right tests run at the right time.

    GitHub Actions supports a wide range of event types, such as push, pull_request, and custom events like repository_dispatch. For our scenario, where we want to selectively trigger UI and API tests, we've opted for custom events.

    In our automation project, we've defined custom event types to represent the specific types of tests we want to run: UI tests and API tests. These event types serve as the bridge between the development repositories and the automation repository. Below is an example of how we've set up custom event types:

                                           
    on:
        repository_dispatch:
            types:
            - execute-ui-tests
            - execute-api-tests
                          
    GitHub Secrets

    In the realm of Continuous Integration and Continuous Delivery (CI/CD), security is paramount, especially when dealing with sensitive information like authentication tokens, API keys, or other credentials. GitHub Actions provides a secure way to handle these sensitive details through a feature known as GitHub secrets.

    GitHub secrets are encrypted environment variables that we can store in our GitHub repository. They are designed to keep sensitive information safe and are a critical part of ensuring that our CI/CD workflows are secure.

    In our configuration, we've used a Personal Access Token (PAT) as a GitHub secret to authenticate with the repositories involved. Here's how we've employed this approach:

    The Trigger Actions

    In GitHub Actions, the uses property is used to specify the action that we want to include and execute in our workflow. An action is a reusable, standalone unit of work that can be used to automate tasks within our workflows. The uses property points to the location of the action's code and configuration files.

    We can use the uses property to reference:

    In our GitHub Actions workflow, we've utilized the github-script@v6 action to execute JavaScript code. This script allows us to interact with GitHub's API, enabling us to automate tasks that require GitHub-related actions, such as triggering workflows in other repositories. Here's how it works:

                                           
    name: Trigger API Tests
    
    on:
        push:
                                
    jobs:
        trigger-api-tests:
        runs-on: ubuntu-latest
        steps:
            - name: Trigger API tests workflow
            uses: actions/github-script@v6
            with:
                github-token: ${{secrets.PAT}}
                script: |
                await github.request('POST /repos/{owner}/{repo}/dispatches', {
                    owner: 'Crypted39',
                    repo: 'Web-App-Automation',
                    event_type: 'execute-api-tests'
                });
                          

    The heart of the action is the JavaScript code block enclosed in the script field. This code accomplishes two main tasks:

    Using the same patter, our trigger-ui-tests.yml file in the UI repository looks like this:

                                           
    name: Trigger UI Tests
    
    on:
        push:
                                
    jobs:
        trigger-ui-tests:
        runs-on: ubuntu-latest
        steps:
            - name: Trigger UI tests workflow
            uses: actions/github-script@v6
            with:
                github-token: ${{secrets.PAT}}
                script: |
                await github.request('POST /repos/{owner}/{repo}/dispatches', {
                    owner: 'Crypted39',
                    repo: 'Web-App-Automation',
                    event_type: 'execute-ui-tests'
                });
                          
    The Receiver Actions

    Currently, when a change is detected in the UI or API development repository, our workflows will trigger an HTTP POST request to the /repos/{owner}/{repo}/dispatches endpoint using the github-script@v6 action, which essentially sends a custom event signal with a specified event type (e.g., "execute-api-tests" or "execute-ui-tests") to indicate that specific tests should be executed.

    The next step for us is to write the workflow file for the automation repository, which will handle the logic for which tests need to be executed at which point.

    Since we already mentioned that for the testing purposes, we are going to use a WebdriverIO based automation project with NodeJS, our workflow steps are going to be:

    To checkout to our automation repository we are going to use the checkout action from the GitHub marketplace and provide the previously created PAT:

                                           
    steps:
        - name: Checkout automation repository
            uses: actions/checkout@v4
            with:
            token: ${{ secrets.PAT }}
                          

    To install Node.js, similarly, we are going to use the setup-node action available on the marketplace to which we will pass our desired version:

                                           
    - name: Install Node.js
    uses: actions/setup-node@v3
    with:
        node-version: 18
                          

    For the required dependencies, we can just execute npm install:

                                           
    - name: Install dependencies
    run: npm install
                          

    To determine which tests we will execute, we will utilize a combination of PowerShell and shell scripting since our workflow is running in a Windows environment.

    By using the GitHub Actions context variable ${{github.event.action}}, which represents the event action that triggered the workflow, we can ascertain which tests we wish to execute:

                                           
    - name: Run UI or API tests
        run: |
            if ("${{github.event.action}}" -eq "execute-ui-tests") {
              npm run ui-tests
            }
            elseif ("${{github.event.action}}" -eq "execute-api-tests") {
              npm run api-tests
            }
            else {
              Write-Output "Unsupported event action: ${{github.event.action}}"
              exit 1
            }
                          

    The script checks the value of github.event.action using the -eq operator, which is used to compare strings in some scripting languages.

    If the value of github.event.action is "execute-ui-tests," it executes npm run ui-tests. And if the value is "execute-api-tests," it executes npm run api-tests.

    In case the value of the github.event.action doesn't match either of these two expected values, it prints a message indicating that the event action is unsupported and exits the step with an error code (exit 1). This ensures that if the event action is not recognized, the workflow fails and doesn't continue with incorrect or unexpected actions.

    Triggering UI Tests

    Now, let's put our setup to the test. When we push a minor change to the UI development repository, the configured workflow will be triggered. Once activated, it will send a custom event signal, known as execute-ui-tests, to the automation repository.

    If we navigate to the Actions tab of our UI development repository, we will see that our workflow was successfully executed:

    ui development repository actions tab

    If we navigate to the Actions tab of our automation repository, we will find a successfully executed workflow listed there as well:

    automation repository actions tab

    When we open the run-tests job, we can observe that under the Run UI or API tests section, only the UI tests have been executed:

    automation workflow job details for UI dispatch

    Triggering API Tests

    Likewise, in the case of the API development repository, when a change is pushed, a workflow will be triggered. However, this time, it will send a custom signal known as execute-api-tests:

    api development repository actions tab

    If we navigate back to the Actions tab of the automation repository, we will now see the second workflow being triggered:

    automation repository actions tab with two workflows

    And if we look at the Run UI or API tests step of the second job, we will see that only the API tests have been executed:

    automation workflow job details for API dispatch

    So, what have we learned:

    GitHub Actions empower efficient and targeted CI/CD workflows by allowing the automation of tasks across multiple repositories.



    Custom event triggers, coupled with GitHub Scripts, enable the orchestration of tests in response to changes, optimizing testing efforts.



    GitHub Secrets provide a secure way to manage sensitive information, such as Personal Access Tokens, ensuring the integrity of our automation setup.



    By automating the triggering of UI and API tests in separate repositories, we achieve a seamless, synchronized, and focused CI/CD strategy.



    This cross-repository CI/CD approach enhances development efficiency, accelerates feedback loops, and maintains a high level of test coverage while promoting security and reliability.

    If you are interested in the complete workflow files, they can all be found on our GitHub repository. Thank you for joining us on this journey of cross-repository CI/CD automation with GitHub Actions. Happy coding and automating!