Conditional Execution in GitHub Actions Based on File Changes

Optimizing Continuous Integration (CI) pipelines is critical for reducing cycle times and minimizing operational costs. In complex project structures, particularly monorepos containing multiple microservices or distinct asset directories, executing every single workflow step on every push is wasteful. When changes are isolated to a specific directory—such as a Terraform configuration or a documentation folder—only the relevant components of the pipeline should trigger.

While GitHub provides native path filters, these operate exclusively at the workflow level, preventing the entire workflow from starting. To achieve granular control over specific jobs or steps within a running workflow, developers must implement custom logic to detect modified files.

Implementing Path Filters via Third-Party Actions

For those seeking a streamlined implementation without writing custom shell scripts, the dorny/paths-filter action is a highly effective solution. This tool allows for the definition of filters that map specific paths to output variables, which can then be used in subsequent steps via the if conditional.

A typical implementation for a Terraform deployment would look as follows:

yaml - uses: dorny/paths-filter@v3 id: changes with: filters: | tf: - 'deploy/tf/**' - run: terraform apply ... if: steps.changes.outputs.tf == 'true'

By utilizing this method, engineers can shave minutes off their pipeline execution, which directly impacts the cost of CI runners and accelerates the feedback loop for developers. Similarly, tj-actions/changed-files is recommended for more complex scenarios, as native git commands can occasionally be error-prone when dealing with edge cases such as rebased pull requests.

Manual Detection Using Native Git Commands

For environments where third-party actions are restricted or where a token-less approach is preferred, native Git commands provide a robust alternative. Since Git is included by default in GitHub Action containers, it can be used to identify modified files through the git diff command.

Using Git Diff for Push Events and Commits

In a standard push event, the most straightforward way to detect changes is to compare the current commit (HEAD) with its immediate predecessor (HEAD^).

Using PowerShell Core (pwsh), this can be implemented by capturing the diff and filtering it with a regular expression:

```powershell

Diff HEAD with the previous commit

$diff = git diff --name-only HEAD^ HEAD

Check if a file under docs/ or with the .md extension has changed

$SourceDiff = $diff | Where-Object { $_ -match '^docs/' -or $_ -match '.md$' }
$HasDiff = $SourceDiff.Length -gt 0

Set the output named "docs_changed"

Write-Host "::set-output name=docs_changed::$HasDiff"
```

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

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

Advanced Diffing for Pull Requests

Pull Requests require a different approach because the diff must be calculated between the base branch (the target of the PR) and the current head of the PR branch. This is achieved by accessing the GitHub context variables github.event.pull_request.base.sha and github.sha.

To ensure the list of files only includes those that are still present in the current state, the --diff-filter=ACMRT flag is used. This filter limits the results to files that were Added, Copied, Modified, Renamed, or changed (T), effectively ignoring deleted files.

A professional shell implementation to filter for specific file types (e.g., CSS) and format them into a single line using xargs looks like this:

bash git diff --name-only --diff-filter=ACMRT ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep .css$ | xargs

Architectural Patterns for Monorepos

In monorepo architectures, the "Check Path Changes" logic is often abstracted into a reusable workflow to avoid duplication across multiple microservices. This allows a primary deployment workflow to call a specialized check job before proceeding to the deployment phase.

Reusable Workflow Structure

A reusable workflow can be defined to take a path_to_check input and return a should_run output. This structure allows the workflow to handle manual triggers (workflow_dispatch) by defaulting should_run to true, ensuring that manual deployments are never blocked by the path filter.

Job Dependency Management

When implementing this at the job level, the needs and if properties are essential. The needs property ensures the deployment job waits for the check job to complete, while the if property evaluates the output of that check.

```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..."
```

Practical Application: Selective Auditing

A real-world application of this logic is found in performance auditing, such as running Lighthouse on a static site. Instead of auditing every page on every commit—which is time-consuming—the pipeline can be configured to:

  1. Retrieve modified files using git diff or tj-actions/changed-files.
  2. Filter for specific web page extensions (e.g., .html, .mdx).
  3. Target only those modified pages for the audit.

To maintain quality, a "global trigger" is often implemented: if critical assets (like style.css or script.js) are changed, the AUDIT_ALL flag is set to true, overriding the selective filter and forcing a full site audit.

Conclusion

The ability to conditionally execute GitHub Action steps based on file changes transforms a blunt CI pipeline into a precise deployment engine. Whether through the simplicity of dorny/paths-filter, the flexibility of reusable workflows for monorepos, or the precision of git diff with specific filters like ACMRT, the goal remains the same: reducing noise and optimizing resource consumption. By implementing these strategies, organizations can significantly reduce CI costs and improve developer velocity by ensuring that only the necessary code is tested and deployed.

Sources

  1. How To Only Run GitHub Actions Steps If Files Change
  2. Executing GitHub Actions jobs or steps only when specific files change
  3. Get changed files in GitHub Actions
  4. Only run GitHub Actions when certain files have changed
  5. Retrieving list of modified files in GitHub Action

Related Posts