The capability to trigger a GitHub Action immediately following the conclusion of another independent action is a critical requirement for complex Continuous Integration and Continuous Deployment (CI/CD) pipelines. In enterprise-grade software engineering, workflows are rarely monolithic; they are decomposed into discrete units such as build, test, security scanning, and deployment. Establishing a reliable sequence where a "Second action" depends on the completion or success of a "First action" ensures that unstable code is not deployed and that resource-intensive tasks only execute when their prerequisites are met. This orchestration is achieved through a variety of mechanisms, ranging from native event triggers like workflow_run to third-party polling actions and job-level dependencies.
Implementing the workflow_run Event Trigger
The workflow_run event is the primary native mechanism provided by GitHub to trigger a workflow based on the activity of another workflow. This is specifically designed for scenarios where two workflows are separate and independent files in the .github/workflows directory but maintain a logical sequence.
To implement this, the dependent workflow (the "Second action") must be configured with a specific on block. In the second-action.yml file, the workflow_run field is used to specify the triggering criteria.
The configuration requires two primary components:
- workflows: This field specifies the exact name of the workflow (or a list of workflows) that will trigger the current execution. For example, if the triggering workflow is named "First action", this field must match that string exactly.
- types: This field limits the trigger to specific activity types. The available types are
completed,requested, andin_progress. To ensure the second action runs only after the first has finished, thecompletedtype must be specified.
The real-world impact of using workflow_run is the creation of a decoupled pipeline. This allows teams to maintain smaller, more manageable YAML files rather than one massive, complex file. However, it introduces a layer of abstraction where the triggering relationship is defined in the downstream workflow rather than the upstream one.
Conditional Execution Based on Workflow Conclusion
While the completed type ensures the second workflow starts after the first finishes, it does not inherently check if the first workflow actually succeeded. By default, workflow_run triggers regardless of whether the previous workflow failed, was cancelled, or succeeded.
To ensure the "Second action" only runs upon the successful completion of the "First action", a conditional statement must be added to the job level using the github.event.workflow_run.conclusion property.
The implementation involves adding an if conditional to the job definition:
yaml
jobs:
deploy:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- run: npm run deploy
This specific configuration creates a quality gate. If the "First action" (e.g., a test suite) fails, the conclusion will not be success, and the deployment job in the second workflow will be skipped. This prevents the deployment of broken code into production environments, safeguarding the stability of the live application.
Polling for Completion with wait-on-check-action
In scenarios where workflow_run is insufficient—such as when working with non-default branches or requiring more granular control over which specific checks must pass—the lewagon/wait-on-check-action is a powerful tool. This action utilizes the GitHub Checks API to poll for the results of specific check runs.
This tool allows a workflow to "pause" its execution until another job or workflow completes successfully. This is particularly useful for complex dependency graphs where a job must wait for a specific set of tests to pass across different workflows.
The action provides several configuration options to refine the waiting process:
- ref: Specifies the git reference (branch or SHA) to monitor. This can be passed as
${{ github.ref }}for the current branch or${{ github.sha }}for the specific commit. - running-workflow-name: Used to identify the specific workflow to monitor.
- check-name: Targets a specific check by its exact name.
- check-regexp: Uses a regular expression to match multiple checks, such as
test-.*to wait for all jobs starting with "test-". - allowed-conclusions: Defines which outcomes are acceptable for the workflow to resume. For example,
success,skipped,cancelledallows the process to continue even if some optional checks were skipped. - ignore-checks: Allows the user to specify checks that should be disregarded, such as
optional-lintorcoverage-report.
The technical implementation for waiting on a specific workflow named "Publish the package" would look as follows:
yaml
- name: Wait for package publish
uses: lewagon/[email protected]
with:
ref: ${{ github.ref }}
running-workflow-name: "Publish the package"
repo-token: ${{ secrets.GITHUB_TOKEN }}
For a more flexible approach using regular expressions to wait for all test jobs:
yaml
- name: Wait for all test jobs
uses: lewagon/[email protected]
with:
ref: ${{ github.sha }}
check-regexp: "test-.*"
repo-token: ${{ secrets.GITHUB_TOKEN }}
Handling Optional Workflows and Edge Cases
A common challenge in CI/CD is the "optional" workflow. This occurs when a Docker image build workflow should only run if specific files change, but a subsequent testing workflow needs that image regardless of whether a new build was triggered.
In these cases, using the fail-on-no-checks parameter in the wait-on-check-action is essential. By default, if the action cannot find a check matching the filter, it fails with the message The requested check was never run against this ref, exiting.... By setting fail-on-no-checks to false, the action will succeed even if no matching checks are found, allowing the pipeline to proceed.
Example configuration for optional checks:
yaml
name: Wait for optional checks
on:
push:
jobs:
wait-for-optional-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Wait on optional tests
uses: lewagon/[email protected]
with:
ref: ${{ github.sha }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
running-workflow-name: wait-for-optional-checks
fail-on-no-checks: false
Intra-Workflow Sequencing with the needs Keyword
When the sequential requirement exists between jobs within the same workflow file, the native needs keyword is the correct architectural choice. This creates a direct dependency graph where a job will not start until all jobs listed in the needs section have completed successfully.
Example of native job sequencing:
yaml
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- run: npm run deploy
In this scenario, the deploy job is explicitly dependent on the test job. If the test job fails, the deploy job is automatically skipped. This is the most efficient method for internal sequencing as it avoids the overhead of API polling and external event triggers.
Advanced Job Contexts and Conditional Dependencies
For more complex scenarios where a job might be skipped but the subsequent job must still execute, GitHub Actions provides the always() function. This can be combined with job outputs to create a sophisticated execution flow.
Consider a scenario where an optional-first-job runs based on a condition. A sequential-second-job should run regardless of whether the first job was skipped or failed, but it must still wait for the first job to finish its attempt.
The implementation uses the needs keyword combined with the always() conditional:
yaml
jobs:
optional-first-job:
if: some.condition
outputs:
testval: ${{ steps.echo_test.outputs.testkey }}
steps:
- id: echo_test
run: echo "testkey=testval" >> $GITHUB_OUTPUT
sequential-second-job:
needs:
- optional-first-job
if: always()
steps:
- run: echo '${{ toJSON(needs) }}'
To further refine this, one can use a more restrictive condition to ensure the second job runs only if the first job did not explicitly fail:
yaml
if: "${{ always() && needs.optional-first-job.result != 'failed' }}"
This ensures that if the first job was skipped, the second one proceeds, but if the first job actually ran and failed, the second one is halted.
Technical Limitations and Performance Considerations
While the methods described provide extensive flexibility, they come with specific technical constraints that engineers must account for to avoid pipeline instability.
Limitations of workflow_run
The workflow_run event has specific constraints:
- Default Branch Restriction: It primarily triggers on the default branch.
- Lack of Atomicity: It triggers once per workflow completion; it is not designed to "wait for all" instances of a workflow to finish if multiple are running.
- Manual Validation: It requires the explicit use of an if condition to check for success, as the event itself only signals completion.
API Rate Limits and Polling
When using wait-on-check-action, the action polls the GitHub Checks API. Frequent polling can lead to hitting GitHub API rate limits, especially in large organizations with many concurrent workflows. To mitigate this, users should increase the wait-interval to reduce the frequency of requests.
Reusable Workflows and Naming
When using the wait-on-check-action within a reusable workflow, the naming convention for checks changes. The check name will include both the caller and the callee job names. For example, if a caller workflow uses a callee workflow, the running-workflow-name must be specified as "caller / callee".
Summary of Sequencing Methods
The following table compares the primary methods for executing GitHub Actions sequentially.
| Method | Use Case | Trigger Mechanism | Scope | Success Requirement |
|---|---|---|---|---|
workflow_run |
Independent workflows | Event-based trigger | Cross-workflow | Manual via if condition |
wait-on-check-action |
Complex/Optional dependencies | API Polling | Cross-workflow | Configurable via allowed-conclusions |
needs keyword |
Direct job dependencies | Internal DAG | Same workflow | Automatic |
always() + needs |
Flexible/Conditional paths | Job Context | Same workflow | Explicit via result check |
Integration with GitHub Enterprise (GHE)
For organizations utilizing GitHub Enterprise Server, the wait-on-check-action requires an additional configuration to point to the local API endpoint. This is achieved using the api-endpoint parameter.
Example for GHE integration:
yaml
- name: Wait for tests (GHE)
uses: lewagon/[email protected]
with:
ref: ${{ github.ref }}
check-name: "Run tests"
repo-token: ${{ secrets.GITHUB_TOKEN }}
api-endpoint: https://github.mycompany.com/api/v3
This ensures that the polling mechanism targets the internal corporate infrastructure rather than the public GitHub API.
Troubleshooting and Diagnostics
When debugging sequential workflows, it is often necessary to inspect the current state of check runs. This can be done using a curl command to the GitHub API, piped through jq for readability:
bash
curl -H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/OWNER/REPO/commits/REF/check-runs \
| jq '[.check_runs[].name]'
This command allows developers to verify the exact names of the checks being reported by GitHub, which is essential for configuring the check-name or running-workflow-name parameters in the wait-on-check-action correctly.
Analysis of Orchestration Strategies
The selection of a sequencing strategy depends entirely on the relationship between the workflows. If the relationship is strict and internal, the needs keyword is the most performant and reliable choice due to its native integration into the GitHub Actions engine. It provides the lowest latency and the most transparent failure state.
However, as architectures evolve toward micro-workflows, the workflow_run event becomes necessary. It allows for a modular approach where the "Deploy" workflow does not need to know the internal details of the "Test" workflow, only that it has completed. The main drawback here is the reliance on the default branch for triggers, which can complicate feature-branch testing strategies.
The wait-on-check-action fills the gap for high-complexity environments. By shifting from an event-driven model to a polling model, it provides a "wait-until" capability that is not natively present in GitHub's event system. This is particularly vital when dealing with optional workflows or when multiple external checks must be synchronized before a final step. The trade-off is the potential for API rate limiting and the added complexity of managing API tokens.
Ultimately, the most robust pipelines employ a hybrid approach: using needs for local job sequencing, workflow_run for primary stage transitions (e.g., Test $\rightarrow$ Deploy), and wait-on-check-action for managing optional or external dependencies.