Various tweaks to GitHub Label notifier
* Move keyword subscriptions to a separate collection
* Minimize false positives by changing how regexp is compiled - so that we only match full words
* Surface information about subscribing to keywords in the UI
Change-Id: Ic18be2e594a7684970d2031743428154ff5bd021
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/128900
Reviewed-by: Alexander Thomas <athom@google.com>
diff --git a/github-label-notifier/functions/README.md b/github-label-notifier/functions/README.md
index d31dc4e..9407ddb 100644
--- a/github-label-notifier/functions/README.md
+++ b/github-label-notifier/functions/README.md
@@ -2,6 +2,9 @@
WebHook endpoint which can react to issues being labeled and send notifications
to subscribers.
+It also can inspect newly opened issues for specific keywords and treat matches
+as if a specific label was assigned to an issue.
+
# Testing
To run local tests use `test.sh` script from this folder.
@@ -35,6 +38,11 @@
allow create: if request.auth.uid != null;
}
+ match /github-keyword-subscriptions/{repo} {
+ allow read: if true;
+ allow write, create, update, delete: if false
+ }
+
match /{document=**} {
allow read, write: if false;
}
@@ -44,7 +52,7 @@
# Implementation Details
-Subscriptions are stored in the Firestore collection called
+Subscriptions to labels are stored in the Firestore collection called
`github-label-subscriptions` with each document at
`github-label-subscriptions/{userId}` path has the following format
@@ -62,9 +70,9 @@
Subscriptions are intended to be managed by users themselves so they are indexed
by Firebase UID issued by Firebase authentification.
-Additionally this collection contains configuration of keyword subscriptions
-for specific repositories at `github-label-subscriptions/{repositoryName}`,
-where `{repositoryName}` is full repository name with `/` replaced with `$`.
+Subscriptions to keywords for specific repositories are stored at
+`github-keyword-subscriptions/{repositoryName}`, where `{repositoryName}`
+is full repository name with `/` replaced with `$`.
```proto
// Represents a subscription to keywords inside issue body. If the match is
@@ -75,7 +83,7 @@
}
```
-Note: security rules prevent editing subscriptions by anybody - so they
+Note: security rules prevent editing keyword subscriptions by anybody - so they
can only be changed via Firebase Console UI.
Mails are sent via SendGrid.
diff --git a/github-label-notifier/functions/lib/subscriptions_db.dart b/github-label-notifier/functions/lib/subscriptions_db.dart
index 1b22136..812d72f 100644
--- a/github-label-notifier/functions/lib/subscriptions_db.dart
+++ b/github-label-notifier/functions/lib/subscriptions_db.dart
@@ -49,7 +49,10 @@
return _makeRegExp().firstMatch(body)?.group(0);
}
- RegExp _makeRegExp() => RegExp(keywords.join('|'), caseSensitive: false);
+ RegExp _makeRegExp() {
+ final keywordsAlt = keywords.map((kw) => '$kw').join('|');
+ return RegExp('(?<=\\b|_)($keywordsAlt)(?=\\b|_)', caseSensitive: false);
+ }
static final _keywordRegExp = RegExp(r'^[\w_/]+$');
static bool _isOk(String keyword) => _keywordRegExp.hasMatch(keyword);
@@ -60,7 +63,7 @@
String repositoryName) async {
final sanitizedRepositoryName = repositoryName.replaceAll('/', r'$');
final subscriptions = await _firestore
- .document('github-label-subscriptions/$sanitizedRepositoryName')
+ .document('github-keyword-subscriptions/$sanitizedRepositoryName')
.get();
if (!subscriptions.exists) {
diff --git a/github-label-notifier/functions/node/index.test.dart b/github-label-notifier/functions/node/index.test.dart
index c9e4337..cbb6d2a 100644
--- a/github-label-notifier/functions/node/index.test.dart
+++ b/github-label-notifier/functions/node/index.test.dart
@@ -55,8 +55,8 @@
],
}));
- await subscriptions
- .document('dart-lang\$webhook-test')
+ await firestore
+ .document('github-keyword-subscriptions/dart-lang\$webhook-test')
.setData(DocumentData.fromMap({
'keywords': [
'jit',
@@ -164,7 +164,10 @@
};
Map<String, dynamic> makeIssueOpenedEvent(
- {int number = 1, String repositoryName = 'dart-lang/webhook-test'}) =>
+ {int number = 1,
+ String repositoryName = 'dart-lang/webhook-test',
+ String body =
+ 'This is an amazing ../third_party/dart/runtime/vm solution'}) =>
{
'action': 'opened',
'issue': {
@@ -172,7 +175,7 @@
'https://github.com/dart-lang/webhook-test/issues/${number}',
'number': number,
'title': 'TEST ISSUE TITLE',
- 'body': 'This is an amazing aot solution',
+ 'body': body,
'user': {
'login': 'hest',
'html_url': 'https://github.com/hest',
@@ -285,7 +288,7 @@
]));
});
- test('ok - single keyword subscriber', () async {
+ test('ok - issue opened', () async {
final rs = await sendEvent(body: makeIssueOpenedEvent());
expect(rs.statusCode, equals(HttpStatus.ok));
expect(sendgridRequests.length, equals(1));
@@ -307,7 +310,40 @@
expect(
rq.body['content']
.firstWhere((c) => c['type'] == 'text/plain')['value'],
- contains('Matches keyword: aot'));
+ contains('Matches keyword: third_party/dart'));
+ });
+
+ test('ok - issue opened - test underscore as word boundary',
+ () async {
+ final rs = await sendEvent(body: makeIssueOpenedEvent(body: 'xyz, something_jit_something_else, foobar'));
+ expect(rs.statusCode, equals(HttpStatus.ok));
+ expect(sendgridRequests.length, equals(1));
+
+ final rq = sendgridRequests.first;
+ expect(rq.headers['authorization'],
+ equals('Bearer ' + Platform.environment['SENDGRID_SECRET']));
+ expect(rq.body['subject'], contains('#1'));
+ expect(rq.body['subject'], contains('TEST ISSUE TITLE'));
+ expect(
+ rq.body['personalizations'],
+ unorderedEquals([
+ {
+ 'to': [
+ {'email': 'second@example.com'}
+ ]
+ }
+ ]));
+ expect(
+ rq.body['content']
+ .firstWhere((c) => c['type'] == 'text/plain')['value'],
+ contains('Matches keyword: jit'));
+ });
+
+ test('ok - issue opened - no matching keyword',
+ () async {
+ final rs = await sendEvent(body: makeIssueOpenedEvent(body: 'xyz, somethingjitsomething_else, foobar'));
+ expect(rs.statusCode, equals(HttpStatus.ok));
+ expect(sendgridRequests.length, equals(0));
});
}
diff --git a/github-label-notifier/ui/README.deploy.md b/github-label-notifier/ui/README.deploy.md
index a9f298b..21a2d79 100644
--- a/github-label-notifier/ui/README.deploy.md
+++ b/github-label-notifier/ui/README.deploy.md
@@ -17,12 +17,4 @@
## Deployment
-To build and deploy, use the following commands:
-
-pub get
-
-webdev build --output web:build/web -r
-
-rm -rf build/web/packages
-
-firebase deploy -P dart-ci --only hosting:dart-github-notifier
+To build and deploy, use `deploy.sh` script.
diff --git a/github-label-notifier/ui/deploy.sh b/github-label-notifier/ui/deploy.sh
new file mode 100755
index 0000000..c06660c
--- /dev/null
+++ b/github-label-notifier/ui/deploy.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+# Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+set -e
+
+webdev build --output web:build/web -r
+rm -rf build/web/packages
+firebase deploy -P dart-ci --only hosting:dart-github-notifier
diff --git a/github-label-notifier/ui/lib/app_component.dart b/github-label-notifier/ui/lib/app_component.dart
index 5b2f7f9..d813f55 100644
--- a/github-label-notifier/ui/lib/app_component.dart
+++ b/github-label-notifier/ui/lib/app_component.dart
@@ -58,12 +58,22 @@
List<String> repoLabels = [];
String selectedLabel = '';
+ // Configuration of the keyword subscription to flutter/flutter repository.
+ String flutterKeywordLabel = '';
+ List<String> flutterKeywords = [];
+
AppComponent(this._subscriptionsService, this._githubService);
@override
Future<Null> ngOnInit() async {
_subscriptionsService.onAuth.listen((_) async {
subscriptions = await _subscriptionsService.getSubscriptions();
+
+ final subscription = await _subscriptionsService.getKeywordSubscription('flutter/flutter');
+ if (subscription != null) {
+ flutterKeywordLabel = subscription.label;
+ flutterKeywords = subscription.keywords;
+ }
});
fetchLabelsForSelectedRepository();
}
diff --git a/github-label-notifier/ui/lib/app_component.html b/github-label-notifier/ui/lib/app_component.html
index e7259e3..01ac468 100644
--- a/github-label-notifier/ui/lib/app_component.html
+++ b/github-label-notifier/ui/lib/app_component.html
@@ -6,44 +6,50 @@
LOGIN
</material-button>
-<div class="mdc-card mdc-card--outlined" *ngIf="isLoggedIn">
- <div class="subscribe-row">
- <div *ngIf="subscriptions.isEmpty">{{userEmail}} has no subscriptions</div>
- <div *ngIf="subscriptions.isNotEmpty">{{userEmail}} subscribed to:
- <ul class="repo-list">
- <li *ngFor="let repo of subscriptions.entries; trackBy: trackByKey">
- <div>{{repo.key}}</div>
- <ul class="label-list">
- <li *ngFor="let label of repo.value">
- <div>
- {{label}}
- <material-button icon (trigger)="unsubscribeFrom(repo.key, label)">
- <material-icon icon="delete" size="small" baseline></material-icon>
- </material-button>
- </div>
- </li>
- </ul>
- </li>
- </ul>
+<div *ngIf="isLoggedIn">
+ <div class="mdc-card mdc-card--outlined" *ngIf="isLoggedIn">
+ <div class="card-row">
+ <div *ngIf="subscriptions.isEmpty">{{userEmail}} has no subscriptions</div>
+ <div *ngIf="subscriptions.isNotEmpty">{{userEmail}} subscribed to:
+ <ul class="repo-list">
+ <li *ngFor="let repo of subscriptions.entries; trackBy: trackByKey">
+ <div>{{repo.key}}</div>
+ <ul class="label-list">
+ <li *ngFor="let label of repo.value">
+ <div>
+ {{label}}
+ <material-button icon (trigger)="unsubscribeFrom(repo.key, label)">
+ <material-icon icon="delete" size="small" baseline></material-icon>
+ </material-button>
+ </div>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <div class="card-row centered">
+ <material-dropdown-select
+ [options]="supportedRepositories"
+ [(selection)]="selectedRepository"
+ buttonText="{{selectedRepository}}"
+ [deselectOnActivate]="false">
+ </material-dropdown-select>
+ <material-auto-suggest-input
+ [selectionOptions]="repoLabels"
+ [(inputText)]="selectedLabel"
+ label="label name">
+ </material-auto-suggest-input>
+ <material-button
+ raised
+ class="subscribe-button blue"
+ (trigger)="subscribe()">
+ subscribe
+ </material-button>
</div>
</div>
- <div class="subscribe-row centered">
- <material-dropdown-select
- [options]="supportedRepositories"
- [(selection)]="selectedRepository"
- buttonText="{{selectedRepository}}"
- [deselectOnActivate]="false">
- </material-dropdown-select>
- <material-auto-suggest-input
- [selectionOptions]="repoLabels"
- [(inputText)]="selectedLabel"
- label="label name">
- </material-auto-suggest-input>
- <material-button
- raised
- class="subscribe-button blue"
- (trigger)="subscribe()">
- subscribe
- </material-button>
+
+ <div class="mdc-card mdc-card--outlined margin-before" *ngIf="flutterKeywordLabel.isNotEmpty">
+ <p class="card-row centered">Subscribing to label <b>{{flutterKeywordLabel}}</b> in <b>flutter/flutter</b> repository would also subscribe you to the following keywords: <b><span *ngFor="let keyword of flutterKeywords; let lastKeyword=last">{{keyword}}<span *ngIf="!lastKeyword">, </span></span></b></p>
</div>
</div>
\ No newline at end of file
diff --git a/github-label-notifier/ui/lib/app_component.scss b/github-label-notifier/ui/lib/app_component.scss
index 533adca..8f26d84 100644
--- a/github-label-notifier/ui/lib/app_component.scss
+++ b/github-label-notifier/ui/lib/app_component.scss
@@ -17,7 +17,7 @@
@include button-dense-theme('.subscribe-button');
}
-.subscribe-row {
+.card-row {
padding: 10px;
}
@@ -26,6 +26,10 @@
margin-right: auto;
}
+.margin-before {
+ margin-top: 1em;
+}
+
ul.repo-list {
li {
list-style-type: none;
diff --git a/github-label-notifier/ui/lib/src/services/subscription_service.dart b/github-label-notifier/ui/lib/src/services/subscription_service.dart
index d6f1d81..d809a8b 100644
--- a/github-label-notifier/ui/lib/src/services/subscription_service.dart
+++ b/github-label-notifier/ui/lib/src/services/subscription_service.dart
@@ -72,6 +72,29 @@
return groupedByRepo;
}
+ /// Fetch configuration of keyword subscription for the specific repository.
+ Future<KeywordSubscription> getKeywordSubscription(
+ String repositoryName) async {
+ final app = await _ensureApp();
+ if (!isLoggedIn) {
+ return null;
+ }
+
+ final sanitizedRepositoryName = repositoryName.replaceAll('/', r'$');
+ final snapshot = await app
+ .firestore()
+ .doc('github-keyword-subscriptions/$sanitizedRepositoryName')
+ .get();
+
+ if (!snapshot.exists) {
+ return null;
+ }
+
+ return KeywordSubscription(
+ label: snapshot.get('label'),
+ keywords: snapshot.get('keywords').cast<String>());
+ }
+
/// Adds a subscription to a label for the current user.
Future<void> subscribeTo(String repositoryName, String labelName) async {
final labelId =
@@ -156,3 +179,14 @@
String toId() => '$repositoryName:$labelName';
}
+
+/// Represents subscription to particular keywords in a repository.
+///
+/// Whenever a new issue is opened that contains one of the keywords
+/// in the body notifier will notify users subscribed to the specified label.
+class KeywordSubscription {
+ final String label;
+ final List<String> keywords;
+
+ KeywordSubscription({this.label, this.keywords});
+}