Conditional Execution of GitHub Actions Based on File Changes

Implementing conditional logic to execute GitHub Action jobs or steps only when specific files are modified is a critical optimization for modern software development. This capability is especially vital in monorepo architectures, where a single repository contains multiple services, libraries, or documentation folders. Without path-based filtering, every commit triggers every workflow, leading to wasted compute resources, increased build times, and unnecessary deployment cycles.

While GitHub Actions provides basic path filtering in the on.push trigger, developers often require more granular control—such as executing specific steps within a job or triggering complex dependent jobs based on the precise nature of the changes.

Path Filtering Strategies for Monorepos

In a monorepo setup, the primary goal is to run slow tasks, such as integration tests or deployments, only for the components that were actually changed. This prevents a change in a documentation folder from triggering a full backend test suite, thereby saving significant time and resources.

There are three primary methods to achieve this: using dedicated community actions, implementing custom shell scripts, or utilizing reusable workflow logic.

Utilizing the dorny/paths-filter Action

The dorny/paths-filter action is a specialized tool designed to enable conditional execution of workflow steps and jobs based on files modified by a pull request, a feature branch, or recently pushed commits.

Implementation Logic

The action works by defining filters that map specific file patterns to output variables. If any file matching the pattern is changed, the corresponding output is set to true.

yaml jobs: changed: name: "Check what files changed" outputs: python: ${{ steps.filter.outputs.python }} workflow: ${{ steps.filter.outputs.workflow }} steps: - name: "Check out the repo" uses: actions/checkout - name: "Examine changed files" uses: dorny/paths-filter@v1 id: filter with: filters: | python: - "**.py" workflow: - ".github/workflows/testsuite.yml"

Technical Configuration and Parameters

The action provides several parameters to fine-tune how changes are detected:

Parameter Description Default / Detail
token Personal access token used for the GitHub REST API. ${{ github.token }}
working-directory Relative path under $GITHUB_WORKSPACE where the repo is checked out. Empty string
list-files Linux shell configuration for escaping unsafe characters. Empty string
predicate-quantifier Overrides the default "at least one pattern" behavior. Default: match at least one

Setting the predicate-quantifier to every ensures that a file is only included if it matches all defined patterns, which is useful for complex exclusions (e.g., matching .ts files in a directory but excluding .md files).

Custom Implementation via Shell Scripts

For those who prefer not to rely on third-party actions, conditional execution can be implemented using git diff combined with PowerShell Core or Bash. This approach involves manually retrieving the list of modified files and setting a workflow command output.

PowerShell Implementation Example

This method is effective for checking if files in a specific directory (like docs/) or with a specific extension (like .md) have changed.

powershell - shell: pwsh id: check_file_changed run: | $diff = git diff --name-only HEAD^ HEAD $SourceDiff = $diff | Where-Object { $_ -match '^docs/' -or $_ -match '.md$' } $HasDiff = $SourceDiff.Length -gt 0 Write-Host "::set-output name=docs_changed::$HasBiff"

To use this output in a subsequent step, the if expression references the step ID:

yaml - shell: pwsh if: steps.check_file_changed.outputs.docs_changed == 'True' run: echo publish docs

Note that actions/checkout must be configured with fetch-depth: 2 to ensure there are enough commits available for the git diff command to compare the current HEAD with the previous commit.

Job-Level Dependency and Conditional Logic

When the decision to run a job depends on file changes, a "checker" job must be established that outputs the status to be consumed by subsequent jobs.

Reusable Checker Job Pattern

A common pattern involves a dedicated check_changes job that uses a reusable workflow to determine if a path has been modified.

```yaml
jobs:
checkchanges:
name: "Check for changes in directory A"
uses: ./.github/workflows/check-path-changes.yml
permissions:
actions: write
contents: read
pull-requests: read
with:
path
to_check: "directory A"

deploy:
name: "Deploy"
needs: [checkchanges]
if: ${{ needs.check
changes.outputs.should_run == 'true' }}
run: |
echo "Deploying..."
```

In this architecture, the needs property ensures the deploy job waits for the check_changes job to complete, while the if property prevents the deployment from executing if no relevant changes were detected.

Technical Pitfalls and Expression Handling

Implementing complex conditional logic in GitHub Actions comes with specific syntax constraints.

The Newline Evaluation Bug

A critical detail regarding GitHub Actions expressions is that newlines can break evaluation. When using YAML literal block scalars (using the | symbol) or folded style (using the > symbol), the resulting expression may contain newlines that lead to unexpected failures.

For example, a multi-line if expression:

yaml if: | ${{ !contains(github.ref, '-notests') && ( needs.changed.outputs.python == 'true' || needs.changed.outputs.workflow == 'true' ) }}

Despite YAML's flexibility, these newlines can cause the expression to fail. The most reliable solution is to collapse the expression into a single line.

Comprehensive Conditionals

Effective filtering often requires combining path changes with branch-name filters. For instance, a workflow might be configured to run only if Python files changed, the workflow file itself was modified, AND the branch name does not contain -notests.

Conclusion

Optimizing GitHub Actions via path filtering transforms a generic CI/CD pipeline into an intelligent, resource-aware system. By leveraging tools like dorny/paths-filter or custom git diff scripts, teams can significantly reduce the "noise" of unnecessary builds. The transition from simple on.push path filters to job-level dependencies and output-based conditionals allows for complex logic, such as bypassing tests on specific branches while still ensuring that changes to the workflow configuration itself trigger a validation run. As monorepos grow, the ability to precisely map file changes to execution paths becomes a primary driver of developer productivity.

Sources

  1. dorny/paths-filter GitHub Repository
  2. Ned Batchelder: Filtering GitHub Actions by Changed Files
  3. Mezi Antou: Executing GitHub Actions Jobs or Steps Only When Specific Files Change
  4. Norday Tech: Only Run GitHub Actions When Certain Files Have Changed
  5. GitHub Community Discussion: Path Filtering for Monorepos

Related Posts