mirror of
https://gitea.com/actions/dorny-paths-filter.git
synced 2026-08-21 16:13:13 +01:00
Compare commits
6 Commits
v4.0.2
...
retry-tran
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fda67ce02f | ||
|
|
4711b7a31b | ||
|
|
65a156e663 | ||
|
|
b41dfa943b | ||
|
|
9af6e5a9d0 | ||
|
|
cae9006b65 |
35
.github/workflows/pull-request-verification.yml
vendored
35
.github/workflows/pull-request-verification.yml
vendored
@@ -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:
|
||||
|
||||
65
README.md
65
README.md
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
1744
dist/index.js
vendored
File diff suppressed because it is too large
Load Diff
48
package-lock.json
generated
48
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
14
src/main.ts
14
src/main.ts
@@ -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[] = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user