11 Commits

Author SHA1 Message Date
Michal Dorner
668c092af3 Merge pull request #279 from wardpeet/patch-1
Add missing predicate-quantifier
2025-11-25 21:31:09 +01:00
Ward Peeters
209e61402d Add missing predicate-quantifier 2025-09-12 22:58:41 +02:00
Michal Dorner
de90cc6fb3 Update dist and CHANGELOG for v3.0.2 2024-03-02 23:11:12 +01:00
Michal Dorner
cf89abdbae Merge pull request #224 from petermetz/feat-filter-predicate-quantifier
feat: add config parameter for predicate quantifier
2024-03-02 22:28:32 +01:00
Peter Somogyvari
f90d5265d6 feat: add config parameter for predicate quantifier
Setting the new 'predicate-quantifier' configuration parameter to 'every'
makes it so that all the patterns have to match a file for it to be
considered changed.

This can be leveraged to ensure that you only build & test software changes
that have real impact on the behavior of the code, e.g. you can set up your
build to run when Typescript/Rust/etc. files are changed but markdown
changes in the diff will be ignored and you consume less resources to build.

The default behavior does not change by the introduction of this feature
so upgrading can be done safely knowing that existing workflows will not
break.

Signed-off-by: Peter Somogyvari <peter.somogyvari@accenture.com>
2024-02-22 12:48:55 -08:00
Michal Dorner
ebc4d7e9eb Update CHANGELOG for v3.0.1 2024-02-15 09:20:42 +01:00
Michal Dorner
45f16f1875 Merge pull request #133 from frouioui/main
Compare base and ref when token is empty
2024-02-15 09:15:48 +01:00
Michal Dorner
5da0e4c086 Merge branch 'master' 2024-02-15 09:13:51 +01:00
Michal Dorner
1441771bbf Update README.md
Add info about v3 release to What's New section
2024-01-25 08:13:18 +01:00
Florent Poinsard
245527a2ef Merge remote-tracking branch 'upstream/master' 2022-10-26 15:40:33 +02:00
Florent Poinsard
5266f0ac59 Compare base and ref when token is empty
Signed-off-by: Florent Poinsard <florent.poinsard@outlook.fr>
2022-04-19 14:29:04 +02:00
7 changed files with 230 additions and 16 deletions

View File

@@ -1,7 +1,13 @@
# Changelog
## v3.0.2
- [Add config parameter for predicate quantifier](https://github.com/dorny/paths-filter/pull/224)
## v3.0.1
- [Compare base and ref when token is empty](https://github.com/dorny/paths-filter/pull/133)
## v3.0.0
- [Update to Node.js 20 ](https://github.com/dorny/paths-filter/pull/210)
- [Update to Node.js 20](https://github.com/dorny/paths-filter/pull/210)
- [Update all dependencies](https://github.com/dorny/paths-filter/pull/215)
## v2.11.1

View File

@@ -72,6 +72,7 @@ For more scenarios see [examples](#examples) section.
## What's New
- New major release `v3` after update to Node 20 [Breaking change]
- Add `ref` input parameter
- Add `list-files: csv` format
- Configure matrix job to run for each folder with changes using `changes` output
@@ -152,6 +153,22 @@ For more information, see [CHANGELOG](https://github.com/dorny/paths-filter/blob
# changes using git commands.
# Default: ${{ github.token }}
token: ''
# Optional parameter to override the default behavior of file matching algorithm.
# By default files that match at least one pattern defined by the filters will be included.
# This parameter allows to override the "at least one pattern" behavior to make it so that
# all of the patterns have to match or otherwise the file is excluded.
# An example scenario where this is useful if you would like to match all
# .ts files in a sub-directory but not .md files.
# The filters below will match markdown files despite the exclusion syntax UNLESS
# you specify 'every' as the predicate-quantifier parameter. When you do that,
# it will only match the .ts files in the subdirectory as expected.
#
# backend:
# - 'pkg/a/b/c/**'
# - '!**/*.jpeg'
# - '!**/*.md'
predicate-quantifier: 'some'
```
## Outputs
@@ -462,6 +479,32 @@ jobs:
</details>
<details>
<summary>Detect changes in folder only for some file extensions</summary>
```yaml
- uses: dorny/paths-filter@v3
id: filter
with:
# This makes it so that all the patterns have to match a file for it to be
# considered changed. Because we have the exclusions for .jpeg and .md files
# the end result is that if those files are changed they will be ignored
# because they don't match the respective rules excluding them.
#
# This can be leveraged to ensure that you only build & test software changes
# that have real impact on the behavior of the code, e.g. you can set up your
# build to run when Typescript/Rust/etc. files are changed but markdown
# changes in the diff will be ignored and you consume less resources to build.
predicate-quantifier: 'every'
filters: |
backend:
- 'pkg/a/b/c/**'
- '!**/*.jpeg'
- '!**/*.md'
```
</details>
### Custom processing of changed files
<details>

View File

@@ -1,4 +1,4 @@
import {Filter} from '../src/filter'
import {Filter, FilterConfig, PredicateQuantifier} from '../src/filter'
import {File, ChangeStatus} from '../src/file'
describe('yaml filter parsing tests', () => {
@@ -117,6 +117,37 @@ describe('matching tests', () => {
expect(pyMatch.backend).toEqual(pyFiles)
})
test('matches only files that are matching EVERY pattern when set to PredicateQuantifier.EVERY', () => {
const yaml = `
backend:
- 'pkg/a/b/c/**'
- '!**/*.jpeg'
- '!**/*.md'
`
const filterConfig: FilterConfig = {predicateQuantifier: PredicateQuantifier.EVERY}
const filter = new Filter(yaml, filterConfig)
const typescriptFiles = modified(['pkg/a/b/c/some-class.ts', 'pkg/a/b/c/src/main/some-class.ts'])
const otherPkgTypescriptFiles = modified(['pkg/x/y/z/some-class.ts', 'pkg/x/y/z/src/main/some-class.ts'])
const otherPkgJpegFiles = modified(['pkg/x/y/z/some-pic.jpeg', 'pkg/x/y/z/src/main/jpeg/some-pic.jpeg'])
const docsFiles = modified([
'pkg/a/b/c/some-pics.jpeg',
'pkg/a/b/c/src/main/jpeg/some-pic.jpeg',
'pkg/a/b/c/src/main/some-docs.md',
'pkg/a/b/c/some-docs.md'
])
const typescriptMatch = filter.match(typescriptFiles)
const otherPkgTypescriptMatch = filter.match(otherPkgTypescriptFiles)
const docsMatch = filter.match(docsFiles)
const otherPkgJpegMatch = filter.match(otherPkgJpegFiles)
expect(typescriptMatch.backend).toEqual(typescriptFiles)
expect(otherPkgTypescriptMatch.backend).toEqual([])
expect(docsMatch.backend).toEqual([])
expect(otherPkgJpegMatch.backend).toEqual([])
})
test('matches path based on rules included using YAML anchor', () => {
const yaml = `
shared: &shared
@@ -186,3 +217,9 @@ function modified(paths: string[]): File[] {
return {filename, status: ChangeStatus.Modified}
})
}
function renamed(paths: string[]): File[] {
return paths.map(filename => {
return {filename, status: ChangeStatus.Renamed}
})
}

View File

@@ -44,6 +44,11 @@ inputs:
This option takes effect only when changes are detected using git against different base branch.
required: false
default: '100'
predicate-quantifier:
description: |
allows to override the "at least one pattern" behavior to make it so that all of the patterns have to match or otherwise the file is excluded.
required: false
default: 'some'
outputs:
changes:
description: JSON array with names of all filters matching any of changed files

69
dist/index.js vendored
View File

@@ -53,16 +53,53 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.Filter = void 0;
exports.Filter = exports.isPredicateQuantifier = exports.SUPPORTED_PREDICATE_QUANTIFIERS = exports.PredicateQuantifier = void 0;
const jsyaml = __importStar(__nccwpck_require__(1917));
const picomatch_1 = __importDefault(__nccwpck_require__(8569));
// Minimatch options used in all matchers
const MatchOptions = {
dot: true
};
/**
* Enumerates the possible logic quantifiers that can be used when determining
* if a file is a match or not with multiple patterns.
*
* The YAML configuration property that is parsed into one of these values is
* 'predicate-quantifier' on the top level of the configuration object of the
* action.
*
* The default is to use 'some' which used to be the hardcoded behavior prior to
* the introduction of the new mechanism.
*
* @see https://en.wikipedia.org/wiki/Quantifier_(logic)
*/
var PredicateQuantifier;
(function (PredicateQuantifier) {
/**
* When choosing 'every' in the config it means that files will only get matched
* if all the patterns are satisfied by the path of the file, not just at least one of them.
*/
PredicateQuantifier["EVERY"] = "every";
/**
* When choosing 'some' in the config it means that files will get matched as long as there is
* at least one pattern that matches them. This is the default behavior if you don't
* specify anything as a predicate quantifier.
*/
PredicateQuantifier["SOME"] = "some";
})(PredicateQuantifier || (exports.PredicateQuantifier = PredicateQuantifier = {}));
/**
* An array of strings (at runtime) that contains the valid/accepted values for
* the configuration parameter 'predicate-quantifier'.
*/
exports.SUPPORTED_PREDICATE_QUANTIFIERS = Object.values(PredicateQuantifier);
function isPredicateQuantifier(x) {
return exports.SUPPORTED_PREDICATE_QUANTIFIERS.includes(x);
}
exports.isPredicateQuantifier = isPredicateQuantifier;
class Filter {
// Creates instance of Filter and load rules from YAML if it's provided
constructor(yaml) {
constructor(yaml, filterConfig) {
this.filterConfig = filterConfig;
this.rules = {};
if (yaml) {
this.load(yaml);
@@ -89,7 +126,16 @@ class Filter {
return result;
}
isMatch(file, patterns) {
return patterns.some(rule => (rule.status === undefined || rule.status.includes(file.status)) && rule.isMatch(file.filename));
var _a;
const aPredicate = (rule) => {
return (rule.status === undefined || rule.status.includes(file.status)) && rule.isMatch(file.filename);
};
if (((_a = this.filterConfig) === null || _a === void 0 ? void 0 : _a.predicateQuantifier) === 'every') {
return patterns.every(aPredicate);
}
else {
return patterns.some(aPredicate);
}
}
parseFilterItemYaml(item) {
if (Array.isArray(item)) {
@@ -528,11 +574,18 @@ async function run() {
const filtersYaml = isPathInput(filtersInput) ? getConfigFileContent(filtersInput) : filtersInput;
const listFiles = core.getInput('list-files', { required: false }).toLowerCase() || 'none';
const initialFetchDepth = parseInt(core.getInput('initial-fetch-depth', { required: false })) || 10;
const predicateQuantifier = core.getInput('predicate-quantifier', { required: false }) || filter_1.PredicateQuantifier.SOME;
if (!isExportFormat(listFiles)) {
core.setFailed(`Input parameter 'list-files' is set to invalid value '${listFiles}'`);
return;
}
const filter = new filter_1.Filter(filtersYaml);
if (!(0, filter_1.isPredicateQuantifier)(predicateQuantifier)) {
const predicateQuantifierInvalidErrorMsg = `Input parameter 'predicate-quantifier' is set to invalid value ` +
`'${predicateQuantifier}'. Valid values: ${filter_1.SUPPORTED_PREDICATE_QUANTIFIERS.join(', ')}`;
throw new Error(predicateQuantifierInvalidErrorMsg);
}
const filterConfig = { predicateQuantifier };
const filter = new filter_1.Filter(filtersYaml, filterConfig);
const files = await getChangedFiles(token, base, ref, initialFetchDepth);
core.info(`Detected ${files.length} changed files`);
const results = filter.match(files);
@@ -555,6 +608,7 @@ function getConfigFileContent(configPath) {
return fs.readFileSync(configPath, { encoding: 'utf8' });
}
async function getChangedFiles(token, base, ref, initialFetchDepth) {
var _a, _b;
// if base is 'HEAD' only local uncommitted changes will be detected
// This is the simplest case as we don't need to fetch more commits or evaluate current/before refs
if (base === git.HEAD) {
@@ -581,8 +635,11 @@ async function getChangedFiles(token, base, ref, initialFetchDepth) {
// At the same time we don't want to fetch any code from forked repository
throw new Error(`'token' input parameter is required if action is triggered by 'pull_request_target' event`);
}
core.info('Github token is not available - changes will be detected from PRs merge commit');
return await git.getChangesInLastCommit();
core.info('Github token is not available - changes will be detected using git diff');
const baseSha = (_a = github.context.payload.pull_request) === null || _a === void 0 ? void 0 : _a.base.sha;
const defaultBranch = (_b = github.context.payload.repository) === null || _b === void 0 ? void 0 : _b.default_branch;
const currentRef = await git.getCurrentRef();
return await git.getChanges(base || baseSha || defaultBranch, currentRef);
}
else {
return getChangedFilesFromGit(base, ref, initialFetchDepth);

View File

@@ -23,6 +23,48 @@ interface FilterRuleItem {
isMatch: (str: string) => boolean // Matches the filename
}
/**
* Enumerates the possible logic quantifiers that can be used when determining
* if a file is a match or not with multiple patterns.
*
* The YAML configuration property that is parsed into one of these values is
* 'predicate-quantifier' on the top level of the configuration object of the
* action.
*
* The default is to use 'some' which used to be the hardcoded behavior prior to
* the introduction of the new mechanism.
*
* @see https://en.wikipedia.org/wiki/Quantifier_(logic)
*/
export enum PredicateQuantifier {
/**
* When choosing 'every' in the config it means that files will only get matched
* if all the patterns are satisfied by the path of the file, not just at least one of them.
*/
EVERY = 'every',
/**
* When choosing 'some' in the config it means that files will get matched as long as there is
* at least one pattern that matches them. This is the default behavior if you don't
* specify anything as a predicate quantifier.
*/
SOME = 'some'
}
/**
* Used to define customizations for how the file filtering should work at runtime.
*/
export type FilterConfig = {readonly predicateQuantifier: PredicateQuantifier}
/**
* An array of strings (at runtime) that contains the valid/accepted values for
* the configuration parameter 'predicate-quantifier'.
*/
export const SUPPORTED_PREDICATE_QUANTIFIERS = Object.values(PredicateQuantifier)
export function isPredicateQuantifier(x: unknown): x is PredicateQuantifier {
return SUPPORTED_PREDICATE_QUANTIFIERS.includes(x as PredicateQuantifier)
}
export interface FilterResults {
[key: string]: File[]
}
@@ -31,7 +73,7 @@ export class Filter {
rules: {[key: string]: FilterRuleItem[]} = {}
// Creates instance of Filter and load rules from YAML if it's provided
constructor(yaml?: string) {
constructor(yaml?: string, readonly filterConfig?: FilterConfig) {
if (yaml) {
this.load(yaml)
}
@@ -62,9 +104,14 @@ export class Filter {
}
private isMatch(file: File, patterns: FilterRuleItem[]): boolean {
return patterns.some(
rule => (rule.status === undefined || rule.status.includes(file.status)) && rule.isMatch(file.filename)
)
const aPredicate = (rule: Readonly<FilterRuleItem>): boolean => {
return (rule.status === undefined || rule.status.includes(file.status)) && rule.isMatch(file.filename)
}
if (this.filterConfig?.predicateQuantifier === 'every') {
return patterns.every(aPredicate)
} else {
return patterns.some(aPredicate)
}
}
private parseFilterItemYaml(item: FilterItemYaml): FilterRuleItem[] {

View File

@@ -4,7 +4,14 @@ import * as github from '@actions/github'
import {GetResponseDataTypeFromEndpointMethod} from '@octokit/types'
import {PushEvent, PullRequestEvent} from '@octokit/webhooks-types'
import {Filter, FilterResults} from './filter'
import {
isPredicateQuantifier,
Filter,
FilterConfig,
FilterResults,
PredicateQuantifier,
SUPPORTED_PREDICATE_QUANTIFIERS
} from './filter'
import {File, ChangeStatus} from './file'
import * as git from './git'
import {backslashEscape, shellEscape} from './list-format/shell-escape'
@@ -26,13 +33,22 @@ async function run(): Promise<void> {
const filtersYaml = isPathInput(filtersInput) ? getConfigFileContent(filtersInput) : filtersInput
const listFiles = core.getInput('list-files', {required: false}).toLowerCase() || 'none'
const initialFetchDepth = parseInt(core.getInput('initial-fetch-depth', {required: false})) || 10
const predicateQuantifier = core.getInput('predicate-quantifier', {required: false}) || PredicateQuantifier.SOME
if (!isExportFormat(listFiles)) {
core.setFailed(`Input parameter 'list-files' is set to invalid value '${listFiles}'`)
return
}
const filter = new Filter(filtersYaml)
if (!isPredicateQuantifier(predicateQuantifier)) {
const predicateQuantifierInvalidErrorMsg =
`Input parameter 'predicate-quantifier' is set to invalid value ` +
`'${predicateQuantifier}'. Valid values: ${SUPPORTED_PREDICATE_QUANTIFIERS.join(', ')}`
throw new Error(predicateQuantifierInvalidErrorMsg)
}
const filterConfig: FilterConfig = {predicateQuantifier}
const filter = new Filter(filtersYaml, filterConfig)
const files = await getChangedFiles(token, base, ref, initialFetchDepth)
core.info(`Detected ${files.length} changed files`)
const results = filter.match(files)
@@ -86,8 +102,11 @@ async function getChangedFiles(token: string, base: string, ref: string, initial
// At the same time we don't want to fetch any code from forked repository
throw new Error(`'token' input parameter is required if action is triggered by 'pull_request_target' event`)
}
core.info('Github token is not available - changes will be detected from PRs merge commit')
return await git.getChangesInLastCommit()
core.info('Github token is not available - changes will be detected using git diff')
const baseSha = github.context.payload.pull_request?.base.sha
const defaultBranch = github.context.payload.repository?.default_branch
const currentRef = await git.getCurrentRef()
return await git.getChanges(base || baseSha || defaultBranch, currentRef)
} else {
return getChangedFilesFromGit(base, ref, initialFetchDepth)
}