Compare commits

...

6 Commits

Author SHA1 Message Date
Sascha Bratton
fda67ce02f Merge branch 'master' into retry-transient-errors 2026-08-04 11:58:58 -04:00
Pavel Kutáč
4711b7a31b feat: add 'some-with-excludes' predicate quantifier (#322) 2026-07-31 17:44:02 -04:00
Sascha Bratton
65a156e663 fix: retry GitHub API requests on transient errors 2026-07-31 14:49:33 -04:00
JiHwan Oh
b41dfa943b docs: add contents permission to PR example (#248)
Co-authored-by: 오지환 (Cooper) <cooper@querypie.com>
2026-07-09 17:57:09 -04:00
Sascha Bratton
9af6e5a9d0 fix: scope base-ignored warning to API path (#319) 2026-07-02 21:33:01 -04:00
Michael Hinton
cae9006b65 docs: update outputs in readme to account for the 'every' predicate-quantifier (#247)
Co-authored-by: Sascha Bratton <sascha@queen.one>
2026-07-02 15:50:34 -04:00
9 changed files with 2088 additions and 63 deletions

View File

@@ -167,6 +167,41 @@ jobs:
if: steps.filter.outputs.local_count != 1
run: exit 1
test-predicate-quantifier-some-with-excludes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: modify working tree
run: |
mkdir -p mobile/.config
echo "TEST" > mobile/main.kt
echo "TEST" > mobile/README.md
echo "TEST" > mobile/.config/lint.json
echo "TEST" > backend.go
git add -A
- uses: ./
id: filter
with:
base: HEAD
list-files: shell
predicate-quantifier: 'some-with-excludes'
filters: |
mobile:
- 'mobile/**'
- '!mobile/**/*.md'
- '!mobile/.config/**'
excludesOnly:
- '!**/*.md'
- name: Print 'mobile_files'
run: echo ${{steps.filter.outputs.mobile_files}}
- name: filter-test
if: |
steps.filter.outputs.mobile != 'true'
|| steps.filter.outputs.mobile_count != 1
|| steps.filter.outputs.mobile_files != 'mobile/main.kt'
|| steps.filter.outputs.excludesOnly != 'false'
run: exit 1
test-change-type:
runs-on: ubuntu-latest
steps:

View File

@@ -81,6 +81,7 @@ For more scenarios see [examples](#examples) section.
## What's New
- Add `some-with-excludes` value of the `predicate-quantifier` input parameter
- Automatic workaround for git `dubious ownership` errors in container jobs
- New major release `v4` after update to Node 24 [Breaking change]
- Add `ref` input parameter
@@ -122,7 +123,8 @@ For more information, see [CHANGELOG](https://github.com/dorny/paths-filter/blob
# introduced by the current branch are considered.
# All files are considered as added if there is no common ancestor with
# base branch or no previous commit.
# This option is ignored if action is triggered by pull_request event.
# This option is ignored if action is triggered by pull_request event,
# unless 'token' is set to an empty string (see the 'token' input below).
# Default: repository default branch (e.g. master)
base: ''
@@ -164,19 +166,24 @@ For more information, see [CHANGELOG](https://github.com/dorny/paths-filter/blob
# It's only used if action is triggered by a pull request event.
# GitHub token from workflow context is used as default value.
# If an empty string is provided, the action falls back to detect
# changes using git commands.
# changes using git commands. In that case, on pull request events
# the 'base' input overrides the pull request base - e.g. set
# base: ${{ github.event.before }} to detect changes since the last push.
# 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.
# Supported values:
# 'some' - File is included if it matches at least one pattern (default).
# 'every' - File is included only if it matches all of the patterns.
# 'some-with-excludes' - File is included if it matches at least one pattern
# and no negated pattern (the ones prefixed with '!').
#
# 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.
# you specify 'every' or 'some-with-excludes' 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/**'
@@ -187,10 +194,17 @@ For more information, see [CHANGELOG](https://github.com/dorny/paths-filter/blob
## Outputs
- For each filter, it sets output variable named by the filter to the text:
- `'true'` - if **any** of changed files matches any of filter rules
- `'false'` - if **none** of changed files matches any of filter rules
- For each filter, it sets an output variable with the name `${FILTER_NAME}_count` to the count of matching files.
- Each filter sets an output variable, named after the filter, whose text value depends on the `predicate-quantifier` setting:
- With `predicate-quantifier: 'some'` (default):
- `'true'` - if **any** changed file matches **at least one** of the filter's rules
- `'false'` - if **no** changed file matches **at least one** of the filter's rules
- With `predicate-quantifier: 'every'`:
- `'true'` - if **any** changed file matches **all** of the filter's rules
- `'false'` - if **no** changed file matches **all** of the filter's rules
- With `predicate-quantifier: 'some-with-excludes'`:
- `'true'` - if **any** changed file matches **at least one** of the filter's rules and **none** of its negated rules
- `'false'` - if **no** changed file matches **at least one** of the filter's rules and **none** of its negated rules
- Each filter sets an output variable with the name `${FILTER_NAME}_count` to the count of matching files.
- If enabled, for each filter it sets an output variable with the name `${FILTER_NAME}_files`. It will contain a list of all files matching the filter.
- `changes` - JSON array with names of all filters matching any of the changed files.
@@ -342,7 +356,8 @@ jobs:
runs-on: ubuntu-latest
# Required permissions
permissions:
pull-requests: read
contents: read # required by actions/checkout
pull-requests: read # required by dorny/paths-filter
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v4
@@ -525,6 +540,32 @@ jobs:
</details>
<details>
<summary>Detect changes in multiple unrelated paths and exclude some file extensions</summary>
```yaml
- uses: dorny/paths-filter@v4
id: filter
with:
# With 'some-with-excludes' a file is matched when it matches at least one pattern
# and none of the negated ones. The filter below therefore matches all the files
# in the 'mobile' folder and the workflow file, but never a markdown file or
# anything in 'mobile/.config'.
#
# An exclusion is final - a file excluded by one pattern can't be included back
# by another one. Consequently, a filter consisting of negated patterns only
# never matches anything.
predicate-quantifier: 'some-with-excludes'
filters: |
mobile:
- 'mobile/**'
- '!mobile/**/*.md'
- '!mobile/.config/**'
- '.github/workflows/test_mobile.yml'
```
</details>
### Custom processing of changed files
<details>

View File

@@ -148,6 +148,99 @@ describe('matching tests', () => {
expect(otherPkgJpegMatch.backend).toEqual([])
})
test('ignores exclusions when using the default predicate quantifier', () => {
const yaml = `
src:
- 'src/**'
- '!**/*.md'
`
const filter = new Filter(yaml)
// A negated pattern is just another pattern for the 'some' quantifier - a markdown file
// inside 'src' still matches 'src/**' and any other file matches the negated pattern.
const files = modified(['src/README.md', 'other/file.txt'])
expect(filter.match(files).src).toEqual(files)
})
test('matches files of every pattern when set to PredicateQuantifier.SOME_WITH_EXCLUDES', () => {
const yaml = `
mobile:
- 'mobile/**'
- '!mobile/**/*.md'
- '!mobile/.config/**'
- '.github/workflows/test_mobile.yml'
`
const filterConfig: FilterConfig = {predicateQuantifier: PredicateQuantifier.SOME_WITH_EXCLUDES}
const filter = new Filter(yaml, filterConfig)
const sourceFiles = modified(['mobile/main.kt', 'mobile/src/some/Activity.kt'])
const workflowFiles = modified(['.github/workflows/test_mobile.yml'])
const docsFiles = modified(['mobile/README.md', 'mobile/docs/some/page.md'])
const configFiles = modified(['mobile/.config/lint.json', 'mobile/.config/nested/lint.json'])
const otherFiles = modified(['backend/main.go', '.github/workflows/test_backend.yml'])
expect(filter.match(sourceFiles).mobile).toEqual(sourceFiles)
expect(filter.match(workflowFiles).mobile).toEqual(workflowFiles)
expect(filter.match(docsFiles).mobile).toEqual([])
expect(filter.match(configFiles).mobile).toEqual([])
expect(filter.match(otherFiles).mobile).toEqual([])
})
test('excludes file with PredicateQuantifier.SOME_WITH_EXCLUDES regardless of the pattern order', () => {
const yaml = `
excludeFirst:
- '!**/*.md'
- 'src/**'
excludeLast:
- 'src/**'
- '!**/*.md'
`
const filterConfig: FilterConfig = {predicateQuantifier: PredicateQuantifier.SOME_WITH_EXCLUDES}
const filter = new Filter(yaml, filterConfig)
const match = filter.match(modified(['src/index.ts', 'src/README.md']))
expect(match.excludeFirst).toEqual(modified(['src/index.ts']))
expect(match.excludeLast).toEqual(modified(['src/index.ts']))
})
test('keeps file excluded with PredicateQuantifier.SOME_WITH_EXCLUDES even if a later pattern includes it', () => {
const yaml = `
src:
- 'src/**'
- '!**/*.md'
- 'src/docs/**'
`
const filterConfig: FilterConfig = {predicateQuantifier: PredicateQuantifier.SOME_WITH_EXCLUDES}
const filter = new Filter(yaml, filterConfig)
const match = filter.match(modified(['src/docs/guide.md', 'src/docs/logo.png']))
expect(match.src).toEqual(modified(['src/docs/logo.png']))
})
test('matches nothing with PredicateQuantifier.SOME_WITH_EXCLUDES when there is no include pattern', () => {
const yaml = `
src:
- '!**/*.md'
`
const filterConfig: FilterConfig = {predicateQuantifier: PredicateQuantifier.SOME_WITH_EXCLUDES}
const filter = new Filter(yaml, filterConfig)
const match = filter.match(modified(['src/index.ts', 'src/README.md']))
expect(match.src).toEqual([])
})
test('treats negated extglob as an include pattern with PredicateQuantifier.SOME_WITH_EXCLUDES', () => {
const yaml = `
backend:
- '!(**/*.tsx|**/*.less)'
`
const filterConfig: FilterConfig = {predicateQuantifier: PredicateQuantifier.SOME_WITH_EXCLUDES}
const filter = new Filter(yaml, filterConfig)
expect(filter.match(modified(['src/server.py'])).backend).toEqual(modified(['src/server.py']))
expect(filter.match(modified(['src/ui.tsx'])).backend).toEqual([])
})
test('matches path based on rules included using YAML anchor', () => {
const yaml = `
shared: &shared
@@ -197,6 +290,61 @@ describe('matching specific change status', () => {
expect(match.addOrModify).toEqual(files)
})
test('respects change status of exclude patterns when set to PredicateQuantifier.SOME_WITH_EXCLUDES', () => {
const yaml = `
src:
- 'src/**'
- deleted: '!src/generated/**'
`
const filterConfig: FilterConfig = {predicateQuantifier: PredicateQuantifier.SOME_WITH_EXCLUDES}
const filter = new Filter(yaml, filterConfig)
const files = [
{status: ChangeStatus.Deleted, filename: 'src/generated/api.ts'},
{status: ChangeStatus.Modified, filename: 'src/generated/api.ts'}
]
const match = filter.match(files)
expect(match.src).toEqual([files[1]])
})
test('matches multiple patterns of single change status when set to PredicateQuantifier.SOME_WITH_EXCLUDES', () => {
const yaml = `
docs: &docs
- '!**/*.md'
src:
- added|modified: 'src/**'
- added|modified: *docs
`
const filterConfig: FilterConfig = {predicateQuantifier: PredicateQuantifier.SOME_WITH_EXCLUDES}
const filter = new Filter(yaml, filterConfig)
const files = [
{status: ChangeStatus.Added, filename: 'src/index.ts'},
{status: ChangeStatus.Added, filename: 'src/README.md'},
{status: ChangeStatus.Deleted, filename: 'src/legacy.ts'}
]
const match = filter.match(files)
expect(match.src).toEqual([files[0]])
})
test('or-es patterns of single change status when using the default predicate quantifier', () => {
const yaml = `
src:
- added|modified: ['src/**', '!**/*.md']
`
const filter = new Filter(yaml)
// Both patterns are OR-ed into a single rule, therefore a markdown file inside 'src'
// matches through 'src/**' and any other file matches through the negated pattern.
const files = [
{status: ChangeStatus.Added, filename: 'src/README.md'},
{status: ChangeStatus.Added, filename: 'other/file.txt'},
{status: ChangeStatus.Deleted, filename: 'src/index.ts'}
]
const match = filter.match(files)
expect(match.src).toEqual([files[0], files[1]])
})
test('matches when using an anchor', () => {
const yaml = `
shared: &shared

View File

@@ -46,7 +46,11 @@ inputs:
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.
allows to override the "at least one pattern" behavior:
'some' - file is included if it matches at least one pattern (default).
'every' - file is included only if it matches all of the patterns.
'some-with-excludes' - file is included if it matches at least one pattern
and no negated pattern (the ones prefixed with '!').
required: false
default: 'some'
outputs:

1744
dist/index.js vendored

File diff suppressed because it is too large Load Diff

48
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@actions/core": "^1.10.0",
"@actions/exec": "^1.1.1",
"@actions/github": "6.0.0",
"@octokit/plugin-retry": "^6.1.0",
"picomatch": "^2.3.1"
},
"devDependencies": {
@@ -199,7 +200,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz",
"integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==",
"dev": true,
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.23.5",
@@ -1391,7 +1391,6 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.1.0.tgz",
"integrity": "sha512-BDa2VAMLSh3otEiaMJ/3Y36GU4qf6GI+VivQ/P41NC6GHcdxpKlqV0ikSZ5gdQsmS3ojXeRx5vasgNTinF0Q4g==",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.0.0",
@@ -1463,6 +1462,38 @@
"@octokit/core": ">=5"
}
},
"node_modules/@octokit/plugin-retry": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.1.0.tgz",
"integrity": "sha512-WrO3bvq4E1Xh1r2mT9w6SDFg01gFmP81nIG77+p/MqW1JeXXgL++6umim3t6x0Zj5pZm3rXAN+0HEjmmdhIRig==",
"license": "MIT",
"dependencies": {
"@octokit/request-error": "^5.0.0",
"@octokit/types": "^13.0.0",
"bottleneck": "^2.15.3"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": "5"
}
},
"node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": {
"version": "24.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
"integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
"license": "MIT"
},
"node_modules/@octokit/plugin-retry/node_modules/@octokit/types": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/@octokit/request": {
"version": "8.1.6",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.6.tgz",
@@ -1895,7 +1926,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"dev": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2296,6 +2326,12 @@
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
},
"node_modules/bottleneck": {
"version": "2.19.5",
"resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
"integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==",
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -2336,7 +2372,6 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001565",
"electron-to-chromium": "^1.4.601",
@@ -2883,7 +2918,6 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
"integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
"dev": true,
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -2939,7 +2973,6 @@
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
"dev": true,
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -3114,7 +3147,6 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz",
"integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==",
"dev": true,
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -4622,7 +4654,6 @@
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true,
"peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@@ -6798,7 +6829,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@@ -31,6 +31,7 @@
"@actions/core": "^1.10.0",
"@actions/exec": "^1.1.1",
"@actions/github": "6.0.0",
"@octokit/plugin-retry": "^6.1.0",
"picomatch": "^2.3.1"
},
"devDependencies": {

View File

@@ -21,6 +21,12 @@ const MatchOptions = {
interface FilterRuleItem {
status?: ChangeStatus[] // Required change status of the matched files
isMatch: (str: string) => boolean // Matches the filename
// Matchers for the individual polarities of the patterns this item was created from.
// They are used only by the PredicateQuantifier.SOME_WITH_EXCLUDES quantifier,
// which has to tell inclusions and exclusions apart. The other quantifiers keep
// evaluating the item as a whole via 'isMatch'.
isInclude?: (str: string) => boolean // Matches any of the patterns which are not negated
isExclude?: (str: string) => boolean // Matches any of the patterns which are negated (e.g. '!**/*.md')
}
/**
@@ -47,7 +53,17 @@ export enum PredicateQuantifier {
* at least one pattern that matches them. This is the default behavior if you don't
* specify anything as a predicate quantifier.
*/
SOME = 'some'
SOME = 'some',
/**
* When choosing 'some-with-excludes' in the config it means that files will get matched if
* at least one of the patterns matches them and none of the negated patterns (the ones
* prefixed with '!') matches them. An exclusion is final - a file excluded by one pattern
* can't be included back by another one.
*
* A filter which consists of negated patterns only never matches anything,
* because there is no pattern which could include a file in the first place.
*/
SOME_WITH_EXCLUDES = 'some-with-excludes'
}
/**
@@ -104,13 +120,35 @@ export class Filter {
}
private isMatch(file: File, patterns: FilterRuleItem[]): boolean {
const aPredicate = (rule: Readonly<FilterRuleItem>): boolean => {
return (rule.status === undefined || rule.status.includes(file.status)) && rule.isMatch(file.filename)
const isStatusMatch = (rule: Readonly<FilterRuleItem>): boolean => {
return rule.status === undefined || rule.status.includes(file.status)
}
if (this.filterConfig?.predicateQuantifier === 'every') {
return patterns.every(aPredicate)
} else {
return patterns.some(aPredicate)
const aPredicate = (rule: Readonly<FilterRuleItem>): boolean => {
return isStatusMatch(rule) && rule.isMatch(file.filename)
}
switch (this.filterConfig?.predicateQuantifier) {
case PredicateQuantifier.EVERY:
return patterns.every(aPredicate)
case PredicateQuantifier.SOME_WITH_EXCLUDES: {
let isIncluded = false
for (const rule of patterns) {
if (!isStatusMatch(rule)) {
continue
}
// Once a file is excluded it stays excluded - no other pattern can include it back.
// Therefore all the patterns have to be evaluated even if the file is already included.
if (rule.isExclude?.(file.filename)) {
return false
}
if (!isIncluded && rule.isInclude?.(file.filename)) {
isIncluded = true
}
}
return isIncluded
}
default:
return patterns.some(aPredicate)
}
}
@@ -120,7 +158,7 @@ export class Filter {
}
if (typeof item === 'string') {
return [{status: undefined, isMatch: picomatch(item, MatchOptions)}]
return [createRuleItem(item)]
}
if (typeof item === 'object') {
@@ -130,14 +168,12 @@ export class Filter {
`Expected [key:string]= pattern:string | string[], but [${key}:${typeof key}]= ${pattern}:${typeof pattern} found`
)
}
return {
status: key
.split('|')
.map(x => x.trim())
.filter(x => x.length > 0)
.map(x => x.toLowerCase()) as ChangeStatus[],
isMatch: picomatch(pattern, MatchOptions)
}
const status = key
.split('|')
.map(x => x.trim())
.filter(x => x.length > 0)
.map(x => x.toLowerCase()) as ChangeStatus[]
return createRuleItem(pattern, status)
})
}
@@ -154,3 +190,25 @@ export class Filter {
function flat<T>(arr: T[][]): T[] {
return arr.reduce((acc, val) => acc.concat(val), [])
}
// Compiles filename pattern(s) of a single filter rule item into matchers.
// Multiple patterns are OR-ed together, which is how picomatch treats an array of globs.
// Patterns are also split by their polarity, so PredicateQuantifier.SOME_WITH_EXCLUDES
// can tell inclusions from exclusions. Note that only a leading '!' negates the whole
// pattern - the '!(...)' extglob is a regular pattern matching everything it doesn't enumerate.
function createRuleItem(patterns: string | string[], status?: ChangeStatus[]): FilterRuleItem {
const matchers = (Array.isArray(patterns) ? patterns : [patterns]).map(pattern =>
picomatch(pattern, MatchOptions, true)
)
// picomatch inverts the result of a matcher created from a negated pattern.
// Inverting it back gives a matcher of the filenames such pattern excludes.
const includes = matchers.filter(matcher => !matcher.state.negated)
const excludes = matchers.filter(matcher => matcher.state.negated)
return {
status,
isMatch: str => matchers.some(matcher => matcher(str)),
isInclude: includes.length > 0 ? str => includes.some(matcher => matcher(str)) : undefined,
isExclude: excludes.length > 0 ? str => excludes.some(matcher => !matcher(str)) : undefined
}
}

View File

@@ -1,6 +1,7 @@
import * as fs from 'fs'
import * as core from '@actions/core'
import * as github from '@actions/github'
import {retry} from '@octokit/plugin-retry'
import {GetResponseDataTypeFromEndpointMethod} from '@octokit/types'
import {MergeGroupEvent, PullRequest, PushEvent} from '@octokit/webhooks-types'
@@ -97,11 +98,13 @@ async function getChangedFiles(token: string, base: string, ref: string, initial
if (ref) {
core.warning(`'ref' input parameter is ignored when action is triggered by pull request event`)
}
if (base) {
core.warning(`'base' input parameter is ignored when action is triggered by pull request event`)
}
const pr = github.context.payload.pull_request as PullRequest
if (token) {
if (base) {
core.warning(
`'base' input parameter is ignored when action is triggered by pull request event and 'token' is provided - set token: '' to detect changes using git diff against 'base'`
)
}
return await getChangedFilesFromApi(token, pr)
}
if (github.context.eventName === 'pull_request_target') {
@@ -111,6 +114,9 @@ async function getChangedFiles(token: string, base: string, ref: string, initial
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 using git diff')
if (base) {
core.info(`Using base '${base}' instead of the pull request base`)
}
const baseSha = github.context.payload.pull_request?.base.sha
const defaultBranch = github.context.payload.repository?.default_branch
const currentRef = await git.getCurrentRef()
@@ -199,7 +205,7 @@ async function getChangedFilesFromGit(base: string, head: string, initialFetchDe
async function getChangedFilesFromApi(token: string, pullRequest: PullRequest): Promise<File[]> {
core.startGroup(`Fetching list of changed files for PR#${pullRequest.number} from GitHub API`)
try {
const client = github.getOctokit(token)
const client = github.getOctokit(token, undefined, retry)
const per_page = 100
const files: File[] = []