The architectural design of GitHub Actions is fundamentally rooted in parallelism. Since its general availability in November 2019, the platform has provided a robust framework for continuous integration and continuous delivery (CI/CD), attracting a massive migration of projects from legacy services such as Travis CI. However, this inherent design—where workflows and jobs are executed in parallel by default—creates a significant operational hurdle for developers who require a strict linear progression of tasks. Achieving sequential execution requires a nuanced understanding of the GitHub Actions hierarchy: the repository contains workflows, workflows contain jobs, and jobs contain steps. While the platform optimizes for speed through concurrency, there are specific technical mechanisms available to enforce a sequential order of operations, ranging from simple job-level constraints to complex cross-workflow daisy-chaining via API calls.
The Hierarchy of Execution and Default Parallelism
To implement sequential logic, one must first understand the structural layers of a GitHub Action. A repository hosts one or more workflows, which are defined as YAML files located specifically within the .github/workflows directory at the root level of the project. Each workflow acts as a container for one or more jobs. These jobs, in turn, execute a series of individual steps.
The default behavior of the platform is to execute all workflows and jobs in a repository in parallel. This means that if multiple workflows are triggered by a single event, such as a push or a pull_request, GitHub will attempt to start all of them simultaneously to minimize the total wall-clock time of the pipeline. For many developers, especially those transitioning from other CI services, this parallelism is counterintuitive when a specific order of operations is required—such as building a Docker image before running integration tests that depend on that specific image.
Sequential Execution at the Step Level
The most basic unit of sequence exists within a single job. Within the scope of one job, all steps are executed sequentially by default. This is the simplest form of linear execution provided by the platform.
- Direct Fact: Steps within a single job are always executed in the order they are defined.
- Impact Layer: This ensures that prerequisite tasks, such as the
actions/checkout@v2step or environment setup, are completed before the actual test or build scripts are executed. - Contextual Layer: Because steps are sequential, developers do not need additional configuration to ensure that a compiler runs before a test suite, provided both are housed within the same job.
Managing Sequential Jobs via Matrix Strategies
When a workflow consists of multiple jobs, GitHub typically runs them in parallel. However, when using a strategy matrix, developers can throttle this concurrency.
By utilizing the jobs.strategy element within the workflow YAML configuration, a developer can define the max-parallel value. Setting max-parallel: 1 forces the jobs to execute one after another rather than simultaneously.
| Configuration Key | Value | Result |
|---|---|---|
jobs.strategy |
max-parallel: 1 |
Jobs execute sequentially |
jobs.strategy |
(Default/Unset) | Jobs execute in parallel |
- Direct Fact: Setting
max-parallel: 1in the strategy block limits the number of parallel jobs to one. - Impact Layer: This prevents the system from overwhelming available runners and ensures that jobs do not conflict with one another if they share a limited external resource.
- Contextual Layer: This is a primary method for limiting parallel jobs without needing to define complex dependencies between every single job in the workflow.
Orchestrating Sequential Workflows via Repository Dispatch
The most complex challenge arises when sequence is required across different workflows. Workflows are essentially isolated from one another; they are not aware of the status or existence of other workflows in the same repository. To bridge this gap and "daisy-chain" workflows, the repository_dispatch API event is utilized.
A repository_dispatch event is a webhook that allows activity outside of the standard GitHub event system (or from within another workflow) to trigger a specific workflow. This is the primary mechanism for sequential workflow execution.
- Direct Fact: The
repository_dispatchAPI call can be used at the end of one workflow to trigger the start of the next. - Impact Layer: This allows a developer to create a strict pipeline where Workflow B only starts after Workflow A has successfully signaled its completion.
- Contextual Layer: This solves the problem where a second workflow—such as one used for testing—depends on a Docker image produced and pushed by a first workflow.
Implementation Logic for Daisy-Chaining Workflows
To implement this sequential flow, the first workflow must be triggered by a standard event, such as a push to the main branch. At the final step of this first workflow, a call is made to the GitHub API to trigger the repository_dispatch event.
For workflows involving more than two sequential stages, a unique trigger must be added as the final step of each workflow in the chain. For example, Workflow 1 triggers Workflow 2, which in turn triggers Workflow 3.
To handle the versioning and integrity of the code across these sequential workflows, the checkout step in the subsequent workflows should be modified. Instead of checking out the default branch, it can be configured to use a specific SHA provided in the payload:
yaml
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.event.client_payload.sha }}
This ensures that the subsequent workflow is running on the exact same commit (SHA) as the workflow that triggered it. This prevents a "race condition" where a new commit is pushed to the main branch between the execution of the first and second workflows, leading to tests being run on different versions of the code.
Handling Failures and Optional Execution
In a standard sequential chain, a failure in an upstream workflow would typically stop the entire process. However, there are scenarios where a developer wants the sequence to continue regardless of whether a previous step failed.
This is achieved by modifying the if: statement in the workflow configuration. By utilizing the if: always() condition, the workflow can be instructed to execute even if previous jobs or steps resulted in a failure.
- Direct Fact: Use
if: always()to ensure a step or job runs regardless of previous outcomes. - Impact Layer: This is critical for "cleanup" tasks or notification steps that must run to release resources or alert developers, even if the build failed.
- Contextual Layer: This contrasts with the default behavior where a failure in a sequential chain would block all subsequent triggered workflows.
Economic Considerations and Resource Management
The decision to implement sequential workflows is often driven not only by technical requirements but also by financial constraints. GitHub Actions is free for public repositories, but private repositories have a limited monthly quota of free minutes (typically 2,000 minutes per month).
Parallel execution of all workflows can rapidly consume this quota, especially in large projects with many complex tests. Sequential execution provides a mechanism to save costs.
- Direct Fact: Private repositories are limited to 2,000 free minutes per month.
- Impact Layer: By enforcing sequential execution, developers can prevent all workflows from executing if an upstream workflow fails. If Workflow A (the build) fails, there is no need to run Workflow B (the tests), thereby saving the minutes that would have been wasted on a guaranteed-to-fail test suite.
- Contextual Layer: This links the technical implementation of
repository_dispatchandmax-parallelto the operational cost of maintaining private software projects.
Addressing the Limitations of Workflow Dispatch
While repository_dispatch is an effective tool, it introduces a specific problem: the "re-run" loop. If a user re-runs a failed workflow in a sequential chain, the repository_dispatch call at the end of that workflow will trigger the subsequent workflow again, even if that subsequent workflow had already succeeded.
To mitigate this, advanced configurations are required to verify that:
1. The next workflow has the same ref and sha.
2. The latest run of the subsequent workflow does not already have a completed status.
This requires deeper integration with the GitHub Actions APIs to list workflows, jobs, and check-suites to verify the current state of the pipeline before triggering the next event.
Summary of Sequential Execution Methods
| Scope | Method | Implementation Detail |
|---|---|---|
| Job Steps | Default Behavior | Steps are naturally sequential |
| Job Matrix | max-parallel: 1 |
Constrains parallel jobs to a single execution thread |
| Workflows | repository_dispatch |
Uses API webhooks to daisy-chain separate YAML files |
Conclusion
The transition of projects to GitHub Actions highlights a fundamental tension between the platform's preference for parallelism and the developer's need for sequentiality. While the platform does not provide a simple "sequential" toggle for workflows, the combination of max-parallel for jobs and repository_dispatch for workflows provides a comprehensive toolkit for orchestration. The ability to pass the sha through the client_payload ensures that the sequence remains tied to a specific version of the code, preventing environment drift. Ultimately, the strategic implementation of these sequential patterns allows developers to not only maintain the logical integrity of their CI/CD pipelines but also to optimize their usage of the 2,000-minute free tier for private repositories by failing fast and avoiding redundant executions.