-
Notifications
You must be signed in to change notification settings - Fork 104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[app_dart] github webhook for create events and branch class initialization #1688
Changes from all commits
b7510a7
26cfd2a
8d5a569
2366432
6da2be6
8fc9653
a39e46d
29106b1
6346639
68bd0af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
// Copyright 2021 The Flutter Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style license that can be | ||
// found in the LICENSE file. | ||
|
||
import 'package:gcloud/db.dart'; | ||
import 'package:github/github.dart'; | ||
|
||
@Kind(name: 'Branch', idType: IdType.String) | ||
class Branch extends Model<String> { | ||
Branch({Key<String>? key, this.lastActivity}) { | ||
parentKey = key?.parent; | ||
id = key?.id; | ||
} | ||
|
||
/// The timestamp (in milliseconds since the Epoch) of the last time | ||
/// when current branch had activity. | ||
@IntProperty(propertyName: 'lastActivity', required: false) | ||
int? lastActivity; | ||
|
||
/// The channel of current branch | ||
@StringProperty(propertyName: 'channel', required: false) | ||
String? channel; | ||
|
||
/// [RepositorySlug] of where this commit exists. | ||
RepositorySlug get slug => RepositorySlug.full(repository); | ||
|
||
String get repository => key.id!.substring(0, key.id!.lastIndexOf('/')); | ||
|
||
String get branch => key.id!.substring(key.id!.lastIndexOf('/') + 1); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a channel field as well? In the future, we can use this to get the current candidate branch for CPs There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, added. I wasn't very sure about the use of channel. I thought if we have a channel name such as beta, then it would already manifest itself as being 'beta' in the branch name field? umm I am just wondering what's the difference between branch name and channel |
||
|
||
@override | ||
String toString() { | ||
final StringBuffer buf = StringBuffer() | ||
..write('$runtimeType(') | ||
..write('id: $id') | ||
..write(', key: ${parentKey == null ? null : key.id}') | ||
..write(', branch: $branch') | ||
..write(', channel: $channel') | ||
..write(', repository: $repository') | ||
..write(', lastActivity: $lastActivity') | ||
..write(')'); | ||
return buf.toString(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
// Copyright 2021 The Flutter Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style license that can be | ||
// found in the LICENSE file. | ||
|
||
import 'dart:async'; | ||
import 'dart:convert'; | ||
|
||
import 'package:cocoon_service/src/service/datastore.dart'; | ||
import 'package:gcloud/db.dart'; | ||
import 'package:github/hooks.dart'; | ||
|
||
import '../model/appengine/branch.dart'; | ||
import '../request_handling/exceptions.dart'; | ||
|
||
class RetryException implements Exception {} | ||
|
||
/// A class to manage GitHub branches. | ||
/// | ||
/// Track branch activities such as branch creation, and helps manage release branches. | ||
class BranchService { | ||
BranchService(this.datastore, {this.rawRequest}); | ||
|
||
DatastoreService datastore; | ||
String? rawRequest; | ||
|
||
/// Parse a create github webhook event, and add it to datastore. | ||
Future<void> handleCreateRequest() async { | ||
final CreateEvent? createEvent = await _getCreateRequestEvent(rawRequest!); | ||
if (createEvent == null) { | ||
throw const BadRequestException('Expected create request event.'); | ||
} | ||
|
||
final String? refType = createEvent.refType; | ||
if (refType == 'tag') { | ||
return; | ||
} | ||
final String? branch = createEvent.ref; | ||
final String? repository = createEvent.repository!.slug().fullName; | ||
final int lastActivity = createEvent.repository!.pushedAt!.millisecondsSinceEpoch; | ||
|
||
final String id = '$repository/$branch'; | ||
final Key<String> key = datastore.db.emptyKey.append<String>(Branch, id: id); | ||
final Branch currentBranch = Branch(key: key, lastActivity: lastActivity); | ||
try { | ||
await datastore.lookupByValue<Branch>(currentBranch.key); | ||
} on KeyNotFoundException { | ||
await datastore.insert(<Branch>[currentBranch]); | ||
} | ||
} | ||
|
||
Future<CreateEvent?> _getCreateRequestEvent(String request) async { | ||
try { | ||
return CreateEvent.fromJson(json.decode(request) as Map<String, dynamic>); | ||
} on FormatException { | ||
return null; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
// Copyright 2021 The Flutter Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style license that can be | ||
// found in the LICENSE file. | ||
|
||
import 'package:cocoon_service/src/model/appengine/branch.dart'; | ||
import 'package:cocoon_service/src/service/branch_service.dart'; | ||
import 'package:cocoon_service/src/service/datastore.dart'; | ||
|
||
import 'package:gcloud/db.dart'; | ||
import 'package:test/test.dart'; | ||
|
||
import '../src/datastore/fake_config.dart'; | ||
import '../src/datastore/fake_datastore.dart'; | ||
import '../src/utilities/webhook_generators.dart'; | ||
|
||
void main() { | ||
late FakeConfig config; | ||
late FakeDatastoreDB db; | ||
late DatastoreService datastoreService; | ||
late BranchService branchService; | ||
|
||
setUp(() { | ||
db = FakeDatastoreDB(); | ||
config = FakeConfig( | ||
dbValue: db, | ||
); | ||
datastoreService = DatastoreService(config.db, 5); | ||
}); | ||
|
||
group('branch service test', () { | ||
test('should add branch to db if db is empty', () async { | ||
expect(db.values.values.whereType<Branch>().length, 0); | ||
final String request = generateCreateBranchEvent('flutter-2.12-candidate.4', 'flutter/flutter'); | ||
branchService = BranchService(datastoreService, rawRequest: request); | ||
await branchService.handleCreateRequest(); | ||
|
||
expect(db.values.values.whereType<Branch>().length, 1); | ||
final Branch branch = db.values.values.whereType<Branch>().single; | ||
expect(branch.repository, 'flutter/flutter'); | ||
expect(branch.branch, 'flutter-2.12-candidate.4'); | ||
}); | ||
|
||
test('should not add duplicate entity if branch already exists in db', () async { | ||
expect(db.values.values.whereType<Branch>().length, 0); | ||
|
||
const String id = 'flutter/flutter/flutter-2.12-candidate.4'; | ||
int lastActivity = DateTime.tryParse("2019-05-15T15:20:56Z")!.millisecondsSinceEpoch; | ||
final Key<String> branchKey = db.emptyKey.append<String>(Branch, id: id); | ||
final Branch currentBranch = Branch(key: branchKey, lastActivity: lastActivity); | ||
db.values[currentBranch.key] = currentBranch; | ||
expect(db.values.values.whereType<Branch>().length, 1); | ||
|
||
final String request = generateCreateBranchEvent('flutter-2.12-candidate.4', 'flutter/flutter'); | ||
branchService = BranchService(datastoreService, rawRequest: request); | ||
await branchService.handleCreateRequest(); | ||
|
||
expect(db.values.values.whereType<Branch>().length, 1); | ||
final Branch branch = db.values.values.whereType<Branch>().single; | ||
expect(branch.repository, 'flutter/flutter'); | ||
expect(branch.branch, 'flutter-2.12-candidate.4'); | ||
}); | ||
|
||
test('should add branch if it is different from previously existing branches', () async { | ||
expect(db.values.values.whereType<Branch>().length, 0); | ||
|
||
const String id = 'flutter/flutter/flutter-2.12-candidate.4'; | ||
int lastActivity = DateTime.tryParse("2019-05-15T15:20:56Z")!.millisecondsSinceEpoch; | ||
final Key<String> branchKey = db.emptyKey.append<String>(Branch, id: id); | ||
final Branch currentBranch = Branch(key: branchKey, lastActivity: lastActivity); | ||
db.values[currentBranch.key] = currentBranch; | ||
|
||
expect(db.values.values.whereType<Branch>().length, 1); | ||
|
||
final String request = generateCreateBranchEvent('flutter-2.12-candidate.5', 'flutter/flutter'); | ||
branchService = BranchService(datastoreService, rawRequest: request); | ||
await branchService.handleCreateRequest(); | ||
|
||
expect(db.values.values.whereType<Branch>().length, 2); | ||
expect(db.values.values.whereType<Branch>().map<String>((Branch b) => b.branch), | ||
containsAll(<String>['flutter-2.12-candidate.4', 'flutter-2.12-candidate.5'])); | ||
}); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no parents for
Branch
, so this is dead codeThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
umm maybe I am wrong, but my understanding is it if we keep the code this way, when we initialize the key using
final Key<String> key = datastore.db.emptyKey.append<String>(Branch, id: id);
, the parentKey will be set todatastore.db.emptyKey
, which is a bit different from anull
parentKey (what we would obtain if we remove this line).When we are looking up and deleting entities, and providing a
currentBranch.key
, we are calling on the getter function ofKey<T> get key => parentKey!.append(runtimeType, id: id);
(which is not modifiable) part of the code to retrieve the key. This line of code asserts on parentKey not beingnull
. If we initialized the value to bedatastore.db.emptyKey
we can pass this check. If we remove this line, parentKey will be defaulted tonull
, and causing the assertion ofparentKey!
to fail when we lookup or delete entities.My understanding is based on how we currently generate and lookup keys. Maybe there is a way to workaround this. I could be wrong but I thought with the current topology we need this line to properly initialize a non null parentKey.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What you're describing is an issue in the tests. We should update the fake datastore to handle null parent keys
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
umm so I have the logic at L115 - L120 of githubwebhook.dart currently looking like this https://github.com/flutter/cocoon/pull/1688/files#diff-3a0003489aad220cc50ef5c753a82ed536839ae6ed0ada1ac8b45c12e3ad1f2aR115-R123 . Maybe there is a workaround for this part but I am not sure how to use null as a base key for
append
to workThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The linked pasted doesn't work.
Can you share the snippet?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one of the sample errors looks like this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the line 106 it points to is from the key model class which is not modifiable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for investigating! This is TD and the real issue is that we allow parent key to be nullable. Can you file a Todo to make parent key non-nullable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I opened an issue at flutter/flutter#100981. In our case, should we just keep the line
parentKey = key?.parent;
to pass tests, or maybe there is a better workaround? Thank you!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SGTM