Skip to content

Commit 1ff702d

Browse files
committed
Extend filter syntax with optional specification of file status: add, modified, deleted (dorny#22)
* Add support for specification of change type (add,modified,delete) * Use NULL as separator in git-diff command output * Improve PR test workflow * Fix the workflow file
1 parent caef9be commit 1ff702d

11 files changed

+390
-83
lines changed

.editorconfig

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
insert_final_newline = true
7+
trim_trailing_whitespace = true
8+
indent_style = space
9+
indent_size = 2

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* text=auto eol=lf

.github/workflows/pull-request-verification.yml

+26
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,29 @@ jobs:
7070
- name: filter-test
7171
if: steps.filter.outputs.any != 'true' || steps.filter.outputs.error == 'true'
7272
run: exit 1
73+
74+
test-change-type:
75+
runs-on: ubuntu-latest
76+
steps:
77+
- uses: actions/checkout@v2
78+
- run: touch add.txt && rm README.md && echo "TEST" > LICENSE && git add -A
79+
- uses: ./
80+
id: filter
81+
with:
82+
token: ''
83+
filters: |
84+
add:
85+
- added: "add.txt"
86+
rm:
87+
- deleted: "README.md"
88+
modified:
89+
- modified: "LICENSE"
90+
any:
91+
- added|deleted|modified: "*"
92+
- name: filter-test
93+
if: |
94+
steps.filter.outputs.add != 'true'
95+
|| steps.filter.outputs.rm != 'true'
96+
|| steps.filter.outputs.modified != 'true'
97+
|| steps.filter.outputs.any != 'true'
98+
run: exit 1

README.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ Supported workflows:
2020
## Usage
2121

2222
Filter rules are defined using YAML format.
23-
Each filter rule is a list of [glob expressions](https://github.com/isaacs/minimatch).
24-
Corresponding output variable will be created to indicate if there's a changed file matching any of the rule glob expressions.
23+
Each filter has a name and set of rules.
24+
Rule is a [glob expressions](https://github.com/isaacs/minimatch).
25+
Optionally you specify if the file should be added, modified or deleted to be matched.
26+
For each filter there will be corresponding output variable to indicate if there's a changed file matching any of the rules.
2527
Output variables can be later used in the `if` clause to conditionally run specific steps.
2628

2729
### Inputs
@@ -30,7 +32,7 @@ Output variables can be later used in the `if` clause to conditionally run speci
3032
- **`base`**: Git reference (e.g. branch name) against which the changes will be detected. Defaults to repository default branch (e.g. master).
3133
If it references same branch it was pushed to, changes are detected against the most recent commit before the push.
3234
This option is ignored if action is triggered by *pull_request* event.
33-
- **`filters`**: Path to the configuration file or directly embedded string in YAML format. Filter configuration is a dictionary, where keys specifies rule names and values are lists of file path patterns.
35+
- **`filters`**: Path to the configuration file or directly embedded string in YAML format.
3436

3537
### Outputs
3638
- For each rule it sets output variable named by the rule to text:
@@ -41,6 +43,7 @@ Output variables can be later used in the `if` clause to conditionally run speci
4143
- minimatch [dot](https://www.npmjs.com/package/minimatch#dot) option is set to true - therefore
4244
globbing will match also paths where file or folder name starts with a dot.
4345
- You can use YAML anchors to reuse path expression(s) inside another rule. See example in the tests.
46+
- It's recommended to put quote your path expressions with `'` or `"`. Otherwise you will get an error if it starts with `*`.
4447
- If changes are detected against the previous commit and there is none (i.e. first push of a new branch), all filter rules will report changed files.
4548
- You can use `base: ${{ github.ref }}` to configure change detection against previous commit for every branch you create.
4649

__tests__/filter.test.ts

+53-15
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
11
import Filter from '../src/filter'
2+
import {File, ChangeStatus} from '../src/file'
23

34
describe('yaml filter parsing tests', () => {
45
test('throws if yaml is not a dictionary', () => {
56
const yaml = 'not a dictionary'
67
const t = () => new Filter(yaml)
78
expect(t).toThrow(/^Invalid filter.*/)
89
})
9-
test('throws on invalid yaml', () => {
10-
const yaml = `
11-
src:
12-
src/**/*.js
13-
`
14-
const t = () => new Filter(yaml)
15-
expect(t).toThrow(/^Invalid filter.*/)
16-
})
1710
test('throws if pattern is not a string', () => {
1811
const yaml = `
1912
src:
@@ -27,13 +20,21 @@ describe('yaml filter parsing tests', () => {
2720
})
2821

2922
describe('matching tests', () => {
23+
test('matches single inline rule', () => {
24+
const yaml = `
25+
src: "src/**/*.js"
26+
`
27+
let filter = new Filter(yaml)
28+
const match = filter.match(modified(['src/app/module/file.js']))
29+
expect(match.src).toBeTruthy()
30+
})
3031
test('matches single rule in single group', () => {
3132
const yaml = `
3233
src:
3334
- src/**/*.js
3435
`
3536
const filter = new Filter(yaml)
36-
const match = filter.match(['src/app/module/file.js'])
37+
const match = filter.match(modified(['src/app/module/file.js']))
3738
expect(match.src).toBeTruthy()
3839
})
3940

@@ -43,7 +44,7 @@ describe('matching tests', () => {
4344
- src/**/*.js
4445
`
4546
const filter = new Filter(yaml)
46-
const match = filter.match(['not_src/other_file.js'])
47+
const match = filter.match(modified(['not_src/other_file.js']))
4748
expect(match.src).toBeFalsy()
4849
})
4950

@@ -55,7 +56,7 @@ describe('matching tests', () => {
5556
- test/**/*.js
5657
`
5758
const filter = new Filter(yaml)
58-
const match = filter.match(['test/test.js'])
59+
const match = filter.match(modified(['test/test.js']))
5960
expect(match.src).toBeFalsy()
6061
expect(match.test).toBeTruthy()
6162
})
@@ -67,7 +68,7 @@ describe('matching tests', () => {
6768
- test/**/*.js
6869
`
6970
const filter = new Filter(yaml)
70-
const match = filter.match(['test/test.js'])
71+
const match = filter.match(modified(['test/test.js']))
7172
expect(match.src).toBeTruthy()
7273
})
7374

@@ -77,7 +78,7 @@ describe('matching tests', () => {
7778
- "**/*"
7879
`
7980
const filter = new Filter(yaml)
80-
const match = filter.match(['test/test.js'])
81+
const match = filter.match(modified(['test/test.js']))
8182
expect(match.any).toBeTruthy()
8283
})
8384

@@ -87,7 +88,7 @@ describe('matching tests', () => {
8788
- "**/*.js"
8889
`
8990
const filter = new Filter(yaml)
90-
const match = filter.match(['.test/.test.js'])
91+
const match = filter.match(modified(['.test/.test.js']))
9192
expect(match.dot).toBeTruthy()
9293
})
9394

@@ -101,7 +102,44 @@ describe('matching tests', () => {
101102
- src/**/*
102103
`
103104
let filter = new Filter(yaml)
104-
const match = filter.match(['config/settings.yml'])
105+
const match = filter.match(modified(['config/settings.yml']))
105106
expect(match.src).toBeTruthy()
106107
})
107108
})
109+
110+
describe('matching specific change status', () => {
111+
test('does not match modified file as added', () => {
112+
const yaml = `
113+
add:
114+
- added: "**/*"
115+
`
116+
let filter = new Filter(yaml)
117+
const match = filter.match(modified(['file.js']))
118+
expect(match.add).toBeFalsy()
119+
})
120+
121+
test('match added file as added', () => {
122+
const yaml = `
123+
add:
124+
- added: "**/*"
125+
`
126+
let filter = new Filter(yaml)
127+
const match = filter.match([{status: ChangeStatus.Added, filename: 'file.js'}])
128+
expect(match.add).toBeTruthy()
129+
})
130+
test('matches when multiple statuses are configured', () => {
131+
const yaml = `
132+
addOrModify:
133+
- added|modified: "**/*"
134+
`
135+
let filter = new Filter(yaml)
136+
const match = filter.match([{status: ChangeStatus.Modified, filename: 'file.js'}])
137+
expect(match.addOrModify).toBeTruthy()
138+
})
139+
})
140+
141+
function modified(paths: string[]): File[] {
142+
return paths.map(filename => {
143+
return {filename, status: ChangeStatus.Modified}
144+
})
145+
}

__tests__/git.test.ts

+23
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,27 @@
11
import * as git from '../src/git'
2+
import {ExecOptions} from '@actions/exec'
3+
import {ChangeStatus} from '../src/file'
4+
5+
describe('parsing of the git diff-index command', () => {
6+
test('getChangedFiles returns files with correct change status', async () => {
7+
const files = await git.getChangedFiles(git.FETCH_HEAD, (cmd, args, opts) => {
8+
const stdout = opts?.listeners?.stdout
9+
if (stdout) {
10+
stdout(Buffer.from('A\u0000LICENSE\u0000'))
11+
stdout(Buffer.from('M\u0000src/index.ts\u0000'))
12+
stdout(Buffer.from('D\u0000src/main.ts\u0000'))
13+
}
14+
return Promise.resolve(0)
15+
})
16+
expect(files.length).toBe(3)
17+
expect(files[0].filename).toBe('LICENSE')
18+
expect(files[0].status).toBe(ChangeStatus.Added)
19+
expect(files[1].filename).toBe('src/index.ts')
20+
expect(files[1].status).toBe(ChangeStatus.Modified)
21+
expect(files[2].filename).toBe('src/main.ts')
22+
expect(files[2].status).toBe(ChangeStatus.Deleted)
23+
})
24+
})
225

326
describe('git utility function tests (those not invoking git)', () => {
427
test('Detects if ref references a tag', () => {

0 commit comments

Comments
 (0)