Skip to content

Commit 74b675d

Browse files
authored
Add filtering
* Add filtering * Update funcignore
1 parent 771f339 commit 74b675d

File tree

8 files changed

+148
-70
lines changed

8 files changed

+148
-70
lines changed

.funcignore

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
*.js.map
22
*.ts
3-
.git*
4-
.vscode
3+
.gitignore
4+
.git/
5+
.github/
6+
.vscode/
57
__azurite_db*__.json
68
__blobstorage__
79
__queuestorage__
@@ -10,5 +12,11 @@ test
1012
tsconfig.json
1113
*.sh
1214
*.zip
13-
README.md
14-
node_modules/.bin
15+
node_modules/.bin
16+
filter.yml.example
17+
LICENSE
18+
CODEOWNERS
19+
.eslint*
20+
*.md
21+
jest.config.js
22+
test/

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,8 @@ __azurite_db*__.json
100100

101101
# local scripts
102102
*.sh
103-
*.zip
103+
*.zip
104+
105+
# filter settings - don't commit this, just commit filter.yml.example
106+
# if you fork this repo, you can uncomment this and commit your own filter.yml
107+
filter.yml

INSTALL.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,36 @@ Once that is done, you can create an Actions workflow on any repository that the
1212

1313
You need to create an Azure Function App, and deploy the Azure Function to it.
1414

15+
Before you deploy, you can choose to set a declarative filter for the GitHub events you want to listen for.
16+
17+
This is done in the `fiter.yml` file, with the format shown in `filter.yml.example` and below:
18+
19+
```yaml
20+
# Path: filter.yml
21+
22+
# filter webhook events by type and payload, declaratively
23+
24+
include:
25+
secret_scanning_alert:
26+
action: [created, dismissed, resolved, reopened]
27+
28+
exclude:
29+
secret_scanning_alert:
30+
action: reopened
31+
secret_scanning_alert_location:
32+
33+
```
34+
35+
The corresponding exclude filter for an event name is applied after the include filter.
36+
37+
This example will include any event named `secret_scanning_alert` with an action of `created`, `dismissed`, or `resolved`, `reopened` and will exclude any event named `secret_scanning_alert` with an action of `reopened`. It will also exclude any event named `secret_scanning_alert_location`.
38+
39+
The presence of an include filter here means that excluding `secret_scanning_alert_location` is redundant, as it will never be included in the first place, but it is included to show the syntax.
40+
41+
If you do not want to use a filter, you can delete the `filter.yml` file, or leave it empty.
42+
43+
You do not need to provide both an `include` and `exclude` key.
44+
1545
### Creating the Functions App
1646

1747
You can use the Azure Portal, the Azure CLI, or the VSCode Azure Functions extension to do this.

filter.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
# Path: filter.yml
2+
13
# filter webhook events by type and payload, declaratively
24

35
include:
46
secret_scanning_alert:
5-
action: [created, dismissed, resolved, reopened]
7+
action: [created, dismissed, resolved]
8+
9+
exclude:
10+
secret_scanning_alert:
11+
action: reopened

filter.yml.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Path: filter.yml
2+
3+
# filter webhook events by type and payload, declaratively
4+
5+
include:
6+
secret_scanning_alert:
7+
action: [created, dismissed, resolved]
8+
9+
exclude:
10+
secret_scanning_alert:
11+
action: reopened

src/functions/app.ts

Lines changed: 9 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,23 @@
11
import { Probot } from "probot";
22
import { EmitterWebhookEvent } from "@octokit/webhooks";
33
import { GitHubAppWebHookPayload } from "./types";
4-
5-
// import * as fs from 'fs';
6-
// import * as yaml from 'yaml';
7-
8-
// // read in filter config, from a YAML file called "filter.yml" in the root of the repo
9-
// // if the file doesn't exist, or is invalid YAML, we handle that gracefully
10-
// let filter_config: any;
11-
12-
// try {
13-
// yaml.parse(fs.readFileSync('filter.yml', 'utf8'));
14-
// } catch (error) {
15-
// console.log(error);
16-
// }
4+
import { eventFilter } from "./filter";
175

186
const setupApp = (app: Probot) => {
197
app.onAny(async (event: EmitterWebhookEvent) => {
208
const payload = event.payload as GitHubAppWebHookPayload;
219

2210
const octokit = await app.auth(payload.installation.id);
2311

24-
// if (filter(context)) {
25-
await octokit.repos.createDispatchEvent({
26-
owner: payload.repository.owner.login,
27-
repo: payload.repository.name,
28-
event_type: event.name,
29-
client_payload: payload as unknown as {[key:string]: unknown},
30-
});
31-
// }
12+
if (eventFilter(event)) {
13+
await octokit.repos.createDispatchEvent({
14+
owner: payload.repository.owner.login,
15+
repo: payload.repository.name,
16+
event_type: event.name,
17+
client_payload: payload as unknown as { [key: string]: unknown },
18+
});
19+
}
3220
});
3321
};
3422

35-
// const filter = (event: EmitterWebhookEvent): boolean => {
36-
// if (filter_config === undefined) return true;
37-
38-
// const event_name = event.name;
39-
40-
// // check if the event type is allowed by the filter config
41-
// // if there is an include list, and this type isn't on it, return false
42-
// // if there is a matching entry in the excludes, and there are no more details under that entry, return false
43-
// if (filter_config.include !== undefined && !(event_name in filter_config.include)
44-
// || filter_config.exclude !== undefined && event_name in filter_config.exclude && Object.keys(filter_config.exclude).length === 0
45-
// ) return false;
46-
47-
// // check the event payload against the filter config's include rule, if it exists
48-
// const include_filter = filter_config.include[event_name];
49-
// const payload = event.payload;
50-
51-
// if (include_filter !== undefined) {
52-
53-
// const matches = Object.keys(include_filter).every(key => {
54-
// if (typeof include_filter[key] === typeof (String)) return payload[key] === include_filter[key];
55-
// if (typeof include_filter[key] === typeof (Array)) return payload[key] in include_filter[key];
56-
// return false;
57-
// });
58-
59-
// if (!matches) return false;
60-
// }
61-
62-
// // same for the exclude rule
63-
// const exclude_filter = filter_config.exclude[event_name];
64-
65-
// if (exclude_filter !== undefined) {
66-
// const matches = Object.keys(exclude_filter).every(key => {
67-
// if (typeof exclude_filter[key] === typeof (String)) return payload[key] === exclude_filter[key];
68-
// if (typeof exclude_filter[key] === typeof (Array)) return payload[key] in exclude_filter[key];
69-
// return false;
70-
// });
71-
72-
// if (matches) return false;
73-
// }
74-
75-
// return true;
76-
// }
77-
7823
export default setupApp;

src/functions/filter.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as fs from 'fs';
2+
import * as yaml from 'yaml';
3+
import { EmitterWebhookEvent } from '@octokit/webhooks';
4+
5+
// read in filter config, from a YAML file called "filter.yml" in the root of the repo
6+
// if the file doesn't exist, or is invalid YAML, we handle that gracefully
7+
let filter_config: any;
8+
9+
try {
10+
filter_config = yaml.parse(fs.readFileSync('filter.yml', 'utf8'));
11+
} catch (error) {
12+
// if the file doesn't exist, or can't be parsed, that's fine, we'll just use the default filter, which is to let everything through
13+
// we do log the error
14+
console.log(error);
15+
}
16+
17+
export const eventFilter = (event: EmitterWebhookEvent): boolean => {
18+
if (filter_config === undefined) return true;
19+
20+
const event_name = event.name;
21+
22+
// check if the event type is allowed by the filter config
23+
// if there is an include list, and this type isn't on it, return false
24+
// if there is a matching entry in the excludes, and there are no more details under that entry, return false
25+
if (filter_config.include !== undefined && !(event_name in filter_config.include)
26+
|| filter_config.exclude !== undefined && event_name in filter_config.exclude && Object.keys(filter_config.exclude).length === 0
27+
) return false;
28+
29+
// check the event payload against the filter config's include and exclude rules
30+
// we can include or exclude by any string-valued key in the payload, and we can match a single string, or an array of options
31+
const payload = event.payload as unknown as { [key: string]: unknown };
32+
33+
const include_filter = filter_config.include[event_name];
34+
if (!applyFilter(payload, include_filter, true)) return false;
35+
36+
const exclude_filter = filter_config.exclude[event_name];
37+
if (applyFilter(payload, exclude_filter, false)) return false;
38+
39+
return true;
40+
}
41+
42+
function applyFilter(payload: { [key: string]: unknown }, filter: { [key: string]: String | Array<String> }, sense: boolean): boolean {
43+
if (filter === undefined) return sense;
44+
45+
const matches: boolean = Object.keys(filter).every(key => {
46+
const payload_value = payload[key] as string;
47+
48+
if (typeof filter[key] === "string") {
49+
return payload_value === filter[key];
50+
} else if (Array.isArray(filter[key])) {
51+
return filter[key].includes(payload_value);
52+
}
53+
console.warn(`Filter is of unexpected type: ${typeof filter[key]}`);
54+
return false;
55+
});
56+
57+
return matches;
58+
}

test/filter.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { EmitterWebhookEvent } from '@octokit/webhooks';
2+
import { eventFilter } from '../src/functions/filter';
3+
4+
describe('eventFilter', () => {
5+
it ('includes an event correctly', () => {
6+
const mockEvent = { name: 'secret_scanning_alert', payload: { action: 'created' } } as unknown as EmitterWebhookEvent;
7+
const allowed = eventFilter(mockEvent);
8+
expect(allowed).toBe(true);
9+
});
10+
11+
it ('excludes an event correctly', () => {
12+
const mockEvent = { name: 'secret_scanning_alert_location', payload: { action: 'created' } } as unknown as EmitterWebhookEvent;
13+
const allowed = eventFilter(mockEvent);
14+
expect(allowed).toBe(false);
15+
});
16+
})

0 commit comments

Comments
 (0)