# tj-actions/changed-files: deciding what a workflow should run on

> A TypeScript GitHub action that answers one question for CI: which paths changed between two commits. How it computes the answer, what the outputs look like, and the setup mistakes it warns about.

**tj-actions/changed-files** — :octocat: Github action to retrieve all (added, copied, modified, deleted, renamed, type changed, unmerged, unknown) files and directories.

- Repository: https://github.com/tj-actions/changed-files
- Stars: 2,727 · Forks: 338
- Language: TypeScript
- License: MIT
- Published: 2026-10-07 · Updated: 2026-10-07 · Language: en
- Canonical page: https://hysenlabs.com/projects/tj-actions-changed-files

## One question, answered either by git or by the REST API

Every use of this action comes down to a single question: which files changed between here and there. The README is unusually clear that the answer can come from two different places. The action either runs git's native diff command locally, or it calls GitHub's REST API to list commits. Which one it uses depends on what your checkout step left on disk, which is why the checkout configuration shows up as the first thing in the important notes.

The value it returns is a list of relative paths from the project root, along with separate lists for added, copied, modified, deleted, renamed, type changed, unmerged and unknown files. That full set of categories is what the repository description enumerates, and having them separated rather than merged is the difference between a workflow that can act on a deletion and one that merely knows a job should re-run.

The comparison points in the feature list are reasonable. It reports an execution time averaging between zero and ten seconds, it works on Linux, macOS and Windows, it handles git submodules, and it supports merge queues. The action also generates escaped JSON output specifically shaped for matrix jobs, which is a detail worth pausing on: writing that JSON by hand inside a workflow expression is genuinely painful, and having the action emit it removes a class of shell quoting bugs.

## The checkout settings that decide whether it works at all

The most valuable part of the README is the block of important notes, because most failures with this action come from misconfiguration upstream rather than from the action itself. The first note concerns push events: when configuring actions/checkout, fetch-depth must be set to 0 or 2 depending on the use case. With the default shallow clone of one commit, there is no history to diff against, and the action has nothing to compare.

The second note is about large repositories. To avoid pulling the entire branch history, the default fetch-depth of 1 that actions/checkout uses for pull_request events is sufficient. So the two situations want opposite things, which is why the note is written per event rather than as one global rule.

The third note covers a mistake that produces silent, confusing behaviour: do not wrap multiline inputs in single or double quotes. The value is already a string separated by newline characters, so adding quotes turns your pattern list into a single pattern containing quote characters. The fourth note is about credentials: if fetch-depth is not 0, persist-credentials needs to be set to true, because the action calls the API to fill in the comparison history that the shallow clone lacks.

## Glob patterns in YAML, including anchors and negation

Restricting change detection to a subset of paths is the reason most teams adopt the action, and the pattern language is where it earns that. Patterns are specified in YAML and matched with glob semantics: globstar, brace expansion and negation are all supported, and the README links to the glob specification and to two example files in the repository's test directory showing the advanced cases.

YAML-specific features are supported too, which is a detail that catches people out in a good way. Anchors and aliases work, so a shared set of source directory patterns can be defined once and referenced in several jobs. Multi-line strings work, which is the mechanism behind the quote warning in the notes: a block scalar gives you a clean newline-separated list. The repository carries a file named test/changed-files.yml in the tree specifically to demonstrate anchors and multi-line strings, and linking to line ranges in that file is a faster way to learn the syntax than any prose description would be.

Beyond the file lists, the action reports whether specific paths changed through boolean outputs, and it lists changed directories with a configurable maximum matching depth and the option to exclude the current directory. That pair of controls is what makes it usable for monorepos, where a change four levels down should still mark a top level package as touched.

## Outputs that can be written to files or fed to a matrix

The outputs section is where the design decisions show. The action can write its results to a designated .txt or .json file for further processing instead of or alongside passing them as step outputs, which matters when the list is long enough to hit environment variable size limits. It can also restore deleted files, either to their previous location or to a location you specify, which is the feature that makes it usable in workflows that check out sparsely and then need an older version of a file to compare against.

The generated JSON for matrix jobs is the other half. A workflow that wants to run a job per changed package, or per changed shard of a test suite, needs a JSON array and the standard fromJSON construct to consume it. Producing that array correctly, escaped, inside a step output is fiddly enough that having the action emit it is a real saving.

One limit is stated plainly in a note near the top of the README. This action identifies files that changed in events such as pull_request, push, merge_group and release, but it does not detect pending uncommitted changes created during the workflow execution. If a later step writes a file and an even later step expects to see that file in the changed list, this action will not show it. The README points at a separate project, tj-actions/verify-changed-files, for that case.

## TypeScript sources, a checked-in dist, and a test suite

The repository is a normal TypeScript project that happens to be packaged as an action. The tree shows src/ for the source, dist/ for the compiled output that GitHub actually runs, test/ and jest.config.js for the test suite, action.yml as the action definition, and a yarn.lock alongside tsconfig.json and .eslintrc.json. There is also a HISTORY.md and a CODE_OF_CONDUCT.md, plus a .whitesource file and a .codacy.yml for code scanning.

The scripts in package.json show the full pipeline rather than just a build. There is a compile step, a prettier format pass, an eslint pass that runs with max-warnings 0, a packaging step that bundles through @vercel/ncc into lib/main.js with a source map and a licenses file, and a jest run with coverage. An `all` script chains build, format, lint, package and test together, which is the local equivalent of what CI runs.

That last point explains why dist/ is committed. Because the published artifact has to exist for GitHub to run it without a build step, the bundled output lives in the repository. The dependencies are modest and tell you what the action really needs: @actions/core and @actions/exec, @actions/github and @octokit/rest for the API path, micromatch for the glob handling, and yaml for parsing pattern inputs. lodash and @stdlib/utils-convert-path handle path manipulation, the latter being relevant on Windows where separators differ.

## Versioning practice and what the releases show

Two facts about the version numbers are worth putting next to each other without picking a winner. The latest published release is v47.0.6, from 2026-04-18, and the two before it are v47.0.5 from 2026-03-03 and v47.0.4 from 2026-02-17. The version field in package.json reads 45.0.8. Both are part of the project as published, and the gap between them is best read as the checkout snapshot lagging behind the release tags rather than as a version conflict.

What the release notes themselves show is more informative than the numbers. Almost every entry in the v47.0.6 and v47.0.5 notes is a Dependabot bump: actions/setup-node from 6.2.0 to 6.3.0, @types/node, codeql-action, download-artifact, upload-artifact, eslint-plugin-jest, prettier. One entry in v47.0.4 is a release-tagger update. That pattern says the maintenance burden is almost entirely dependency hygiene, which is the expected steady state for an action whose own code rarely changes.

The action reports 31 contributors in its badge, and the repository is not archived with the last push on 2026-09-23. Pinning behaviour is the practical decision this leaves open: pinning to a major version tag gets security fixes, while pinning to a full release tag gives byte-identical behaviour across runs. The README's versioning section is where that choice is documented, and for an action that gates your whole CI pipeline, the conservative option is usually the full tag.

## Conclusion

changed-files earns its place because path filtering in a workflow is a solved problem here rather than a shell script you maintain. The README's own warnings are the ones to internalize: fetch-depth has to be 0 or 2 for push events, multiline inputs must not be wrapped in quotes, and the action will not see files that are modified during the run itself. Everything else is iteration on a mature codebase, with release v47.0.6 published on 2026-04-18 and most recent changes arriving as dependency bumps from Dependabot. Start with the fetch-depth setting, then read the outputs section rather than the examples, since the names of those outputs are what every downstream step keys off.

## FAQ

### Why does changed-files return nothing on a push event?

Almost always because of the checkout depth. The README states that when configuring actions/checkout for push events, fetch-depth must be set to 0 or 2 depending on the use case. The default shallow clone leaves no history to diff against, so the action has nothing to compare and reports no changed files.

### How do you exclude files from change detection?

Use glob patterns in YAML with negation support, which the README lists alongside globstar and brace expansion. YAML anchors and aliases are also supported, so a shared list of paths can be defined once and reused. Avoid wrapping multiline pattern values in quotes, since the value is already a string separated by newline characters.

### Does changed-files see files generated during the workflow run?

No. The README states plainly that the action identifies files that changed in events such as pull_request, push, merge_group and release, but it does not detect pending uncommitted changes created during the workflow execution. For that case the README points to a separate project, tj-actions/verify-changed-files.

### How do you run a separate job for each changed package?

The action generates escaped JSON output specifically shaped for matrix jobs, which you consume with the standard fromJSON construct in the strategy matrix. It also reports whether specific paths changed through boolean outputs, so a simpler alternative is one if condition per package.

## Sources

- [Issues](https://github.com/tj-actions/changed-files/issues)
- [License: MIT](https://github.com/tj-actions/changed-files/blob/main/LICENSE)
- [README](https://github.com/tj-actions/changed-files/blob/main/README.md)
- [Releases](https://github.com/tj-actions/changed-files/releases)
- [tj-actions/changed-files on GitHub](https://github.com/tj-actions/changed-files)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tj-actions-changed-files
