Modern CI/CD pipelines, particularly in monorepo architectures, often suffer from inefficiency when every minor change triggers a full suite of tests and deployments. By implementing conditional execution based on specific file changes, developers can significantly reduce resource consumption and execution time. This is achieved by leveraging specialized GitHub Actions that identify modified files relative to target branches, commits, or tags, allowing workflows to skip irrelevant tasks.
Technical Mechanisms for Change Detection
The process of identifying changed files typically relies on two primary mechanisms: the GitHub REST API and the native Git diff command. The choice between these often depends on the event triggering the workflow. For instance, while the REST API is highly efficient for pull_request events, it may not be supported for push events, necessitating the use of the local .git directory and native Git commands.
A robust implementation, such as tj-actions/changed-files, provides a scalable solution that handles large repositories and supports Git submodules. It is designed to operate across various event types, including pull_request*, push, merge_group, and release. It is important to note that these actions identify changes existing in the commit history; they cannot detect pending uncommitted changes created during the actual execution of the workflow.
Implementation with tj-actions/changed-files
The tj-actions/changed-files action is a versatile tool for tracking changes relative to a target branch, the current branch, or custom commits. It offers high performance, typically executing within 0 to 10 seconds.
Input Configuration and Filtering
Users can precisely control which files are monitored using the files input. This supports complex glob patterns and exclusions.
yaml
- name: Get changed files
id: changed-files
uses: tj-actions/[email protected]
with:
files: |
my-file.txt
*.sh
*.png
!*.md
test_directory/**
**/*.sql
Output Handling and Data Integration
The action provides several outputs to facilitate downstream logic. The most critical is any_changed, a boolean string used in if conditionals to determine if a subsequent step should run.
For more complex data processing, the action supports:
- Custom separators: Changing the default whitespace separator to a comma via the separator input.
- File exports: Writing outputs directly to .txt or .json files located in .github/outputs/ when write_output_files is enabled.
- JSON generation: Generating escaped JSON output specifically for use in matrix jobs.
- Safety controls: The safe_output input can be set to false when storing results in environment variables to prevent command injection.
Practical Workflow Examples
The following table summarizes common use cases for identifying changes.
| Use Case | Requirement | Implementation Detail |
|---|---|---|
| Tag-based Release | Changes between tags | Trigger on push with v* tags; fetch-depth: 0 |
| Specific Directory | Changes in .github/ |
Use files: .github/** and check any_changed == 'true' |
| External Path | Files in dir1 |
Use path: dir1 within the action configuration |
| Conditional Step | Run if specific file changed | Use if: steps.changed-files-specific.outputs.any_changed == 'true' |
Conditional Execution via dorny/paths-filter
Another prominent approach is the dorny/paths-filter action, which focuses specifically on enabling conditional execution of steps and jobs. This is particularly useful for mapping specific file patterns to named outputs.
In a typical implementation, a "filter" job is created to define which paths correspond to which technology stack.
yaml
jobs:
changed:
name: "Check what files changed"
outputs:
python: ${{ steps.filter.outputs.python }}
workflow: ${{ steps.filter.outputs.workflow }}
steps:
- uses: actions/checkout
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
python:
- "**.py"
workflow:
- ".github/workflows/testsuite.yml"
This allows subsequent jobs to use complex logic to determine if they should run. For example, a test suite can be configured to run only if Python files or the workflow file itself changed, while also checking for the absence of specific branch keywords like -notests.
yaml
tests:
needs: changed
if: |
${{
!contains(github.ref, '-notests')
&& (
needs.changed.outputs.python == 'true'
|| needs.changed.outputs.workflow == 'true'
)
}}
Comparison of Change Detection Strategies
Choosing the right tool depends on whether the goal is simple file listing or complex job orchestration.
tj-actions/changed-files: Best for detailed file lists, handling large mono-repos, and generating artifacts (JSON/TXT) for further processing. It provides granular control over the diffing process and supports a wide range of Git references.dorny/paths-filter: Optimized for "boolean" logic—determining whether a specific part of the project was touched to decide if a job should be skipped entirely.
Conclusion
Integrating file-change detection into GitHub Actions transforms a generic CI pipeline into an intelligent, resource-aware system. By shifting from a "run everything" model to a "run what changed" model, teams can drastically reduce the feedback loop for developers. The choice between using a comprehensive tool like tj-actions/changed-files for detailed auditing and dorny/paths-filter for job gating allows for a highly optimized DevOps lifecycle, ensuring that compute resources are allocated only where modifications have occurred.