How does on.paths really work?

We make heavy use of

on:
  push:
    paths:
      - "something/**"

For scoping Workflows related to some sub-component of our monorepository to only run when code within that component has changed.

We find very frequently the Workflows run when we’re not expecting them to. Is there any documentation on exactly how the “changed paths” list that is matched to this glob is computed?

From a user’s perspective, I’d expect it to match the list of changed files in the PR diff view at the time of push. Technically speaking, I’d expect it to be:

mb=$(git merge-base "$base_branch" HEAD)
git diff --name-only "$mb"

But we see these scoped Workflows run when there are no matched paths by those approaches.

Anecdotally, it seems to be related to syncing from the base branch. When we merge in main or rebase + force-push is when the Workflows seem to run unnecessarily.

Note: If you push more than 1,000 commits, or if GitHub does not generate the diff due to a timeout (diffs that are too large diffs), the workflow will always run.

(Amusingly, this isn’t an edge I’ve hit much. I do regularly hit the 250 commit limit.)

GitHub generates the list of changed files using two-dot diffs for pushes and three-dot diffs for pull requests:

  • Pull requests: Three-dot diffs are a comparison between the most recent version of the topic branch and the commit where the topic branch was last synced with the base branch.
  • Pushes to existing branches: A two-dot diff compares the head and base SHAs directly with each other.
  • Pushes to new branches: A two-dot diff against the parent of the ancestor of the deepest commit pushed.
1 Like

Thanks for finding that! Next time we find unexpected runs, I’ll look to see if this explains it.

1 Like