[Gemini Log Analyzer] Part 1: Backend Foundation (Models, Config, & Setup) (#5042)
This is Part 1 of a stacked PR series implementing the Gemini Log Analyzer feature: https://github.com/flutter/flutter/issues/185700
This PR establishes the required foundation:
- Adds `APP_DART_GEMINI_LOG_ANALYZER_KEY` secret config property to `Config`.
- Adds `log_analysis` and `build_id` fields to Firestore `PresubmitJob` model.
- Adds `build_id` to `PresubmitJobResponse` RPC model in `cocoon_common`.
### Stacked PR Series:
- **Part 1: Backend Foundation** (This PR)
- Part 2: Backend Core API
- Part 3: Frontend Dashboard UI
diff --git a/app_dart/lib/src/model/common/presubmit_completed_check.dart b/app_dart/lib/src/model/common/presubmit_completed_check.dart
index b510273..3251307 100644
--- a/app_dart/lib/src/model/common/presubmit_completed_check.dart
+++ b/app_dart/lib/src/model/common/presubmit_completed_check.dart
@@ -41,6 +41,7 @@
final int? endTime;
final String? summary;
final int? buildNumber;
+ final int? buildId;
const PresubmitCompletedJob({
required this.name,
@@ -59,6 +60,7 @@
this.endTime,
this.summary,
this.buildNumber,
+ this.buildId,
});
/// Creates a [PresubmitCompletedJob] from a GitHub [CheckRun].
@@ -81,6 +83,7 @@
endTime: null,
summary: null,
buildNumber: null,
+ buildId: null,
);
}
@@ -107,6 +110,7 @@
endTime: build.endTime.toDateTime().millisecondsSinceEpoch,
summary: build.summaryMarkdown,
buildNumber: build.number,
+ buildId: build.id.toInt(),
);
}
@@ -149,6 +153,7 @@
endTime: endTime,
summary: summary,
buildNumber: buildNumber,
+ buildId: buildId,
);
}
diff --git a/app_dart/lib/src/model/common/presubmit_job_state.dart b/app_dart/lib/src/model/common/presubmit_job_state.dart
index 84582c7..b9756b1 100644
--- a/app_dart/lib/src/model/common/presubmit_job_state.dart
+++ b/app_dart/lib/src/model/common/presubmit_job_state.dart
@@ -20,6 +20,7 @@
final int? endTime;
final String? summary;
final int? buildNumber;
+ final int? buildId;
const PresubmitJobState({
required this.jobName,
@@ -29,6 +30,7 @@
this.endTime,
this.summary,
this.buildNumber,
+ this.buildId,
});
}
@@ -41,5 +43,6 @@
endTime: endTime.toDateTime().millisecondsSinceEpoch,
summary: summaryMarkdown,
buildNumber: number,
+ buildId: id.toInt(),
);
}
diff --git a/app_dart/lib/src/model/firestore/presubmit_job.dart b/app_dart/lib/src/model/firestore/presubmit_job.dart
index 5c285fa..80fc5ea 100644
--- a/app_dart/lib/src/model/firestore/presubmit_job.dart
+++ b/app_dart/lib/src/model/firestore/presubmit_job.dart
@@ -104,12 +104,14 @@
static const fieldSlug = 'slug';
static const fieldJobName = 'job_name';
static const fieldBuildNumber = 'build_number';
+ static const fieldBuildId = 'build_id';
static const fieldStatus = 'status';
static const fieldAttemptNumber = 'attempt_number';
static const fieldCreationTime = 'creation_time';
static const fieldStartTime = 'start_time';
static const fieldEndTime = 'end_time';
static const fieldSummary = 'summary';
+ static const fieldLogAnalysis = 'log_analysis';
static AppDocumentId<PresubmitJob> documentIdFor({
required RepositorySlug slug,
@@ -168,9 +170,11 @@
required int attemptNumber,
required int creationTime,
int? buildNumber,
+ int? buildId,
int? startTime,
int? endTime,
String? summary,
+ String? logAnalysis,
}) {
return PresubmitJob._(
{
@@ -178,12 +182,14 @@
fieldCheckRunId: checkRunId.toValue(),
fieldJobName: jobName.toValue(),
fieldBuildNumber: ?buildNumber?.toValue(),
+ fieldBuildId: ?buildId?.toValue(),
fieldStatus: status.value.toValue(),
fieldAttemptNumber: attemptNumber.toValue(),
fieldCreationTime: creationTime.toValue(),
fieldStartTime: ?startTime?.toValue(),
fieldEndTime: ?endTime?.toValue(),
fieldSummary: ?summary?.toValue(),
+ fieldLogAnalysis: ?logAnalysis?.toValue(),
},
name: documentNameFor(
slug: slug,
@@ -213,9 +219,11 @@
creationTime: creationTime,
status: TaskStatus.waitingForBackfill,
buildNumber: null,
+ buildId: null,
startTime: null,
endTime: null,
summary: null,
+ logAnalysis: null,
);
}
@@ -240,6 +248,9 @@
int? get buildNumber => fields[fieldBuildNumber] != null
? int.parse(fields[fieldBuildNumber]!.integerValue!)
: null;
+ int? get buildId => fields[fieldBuildId] != null
+ ? int.parse(fields[fieldBuildId]!.integerValue!)
+ : null;
int? get startTime => fields[fieldStartTime] != null
? int.parse(fields[fieldStartTime]!.integerValue!)
: null;
@@ -247,6 +258,7 @@
? int.parse(fields[fieldEndTime]!.integerValue!)
: null;
String? get summary => fields[fieldSummary]?.stringValue;
+ String? get logAnalysis => fields[fieldLogAnalysis]?.stringValue;
TaskStatus get status {
final rawValue = fields[fieldStatus]!.stringValue!;
@@ -273,6 +285,14 @@
}
}
+ set buildId(int? buildId) {
+ if (buildId == null) {
+ fields.remove(fieldBuildId);
+ } else {
+ fields[fieldBuildId] = buildId.toValue();
+ }
+ }
+
set summary(String? summary) {
if (summary == null) {
fields.remove(fieldSummary);
@@ -281,8 +301,17 @@
}
}
+ set logAnalysis(String? logAnalysis) {
+ if (logAnalysis == null) {
+ fields.remove(fieldLogAnalysis);
+ } else {
+ fields[fieldLogAnalysis] = logAnalysis.toValue();
+ }
+ }
+
void updateFromBuild(bbv2.Build build) {
fields[fieldBuildNumber] = build.number.toValue();
+ fields[fieldBuildId] = Value(integerValue: build.id.toString());
fields[fieldCreationTime] = build.createTime
.toDateTime()
.millisecondsSinceEpoch
diff --git a/app_dart/lib/src/request_handlers/get_presubmit_jobs.dart b/app_dart/lib/src/request_handlers/get_presubmit_jobs.dart
index b770dc8..bb16a8e 100644
--- a/app_dart/lib/src/request_handlers/get_presubmit_jobs.dart
+++ b/app_dart/lib/src/request_handlers/get_presubmit_jobs.dart
@@ -100,6 +100,7 @@
status: job.status,
summary: job.summary,
buildNumber: job.buildNumber,
+ buildId: job.buildId,
),
];
diff --git a/app_dart/lib/src/service/config.dart b/app_dart/lib/src/service/config.dart
index bf76c4d..24e8396 100644
--- a/app_dart/lib/src/service/config.dart
+++ b/app_dart/lib/src/service/config.dart
@@ -259,6 +259,9 @@
Future<String> get discordTreeStatusWebhookUrl =>
_getSingleValue('TREE_STATUS_DISCORD_WEBHOOK_URL');
+ Future<String> get geminiLogAnalyzerKey =>
+ _getSingleValue('APP_DART_GEMINI_LOG_ANALYZER_KEY');
+
String get wrongBaseBranchPullRequestMessage =>
'This pull request was opened against a branch other than '
'_{{default_branch}}_. Since Flutter pull requests should not '
diff --git a/app_dart/lib/src/service/firestore/unified_check_run.dart b/app_dart/lib/src/service/firestore/unified_check_run.dart
index 05454aa..fea62cc 100644
--- a/app_dart/lib/src/service/firestore/unified_check_run.dart
+++ b/app_dart/lib/src/service/firestore/unified_check_run.dart
@@ -584,6 +584,7 @@
} else if (state.status == TaskStatus.inProgress) {
presubmitJob.startTime = state.startTime!;
presubmitJob.buildNumber = state.buildNumber;
+ presubmitJob.buildId = state.buildId;
// If the job is not completed, update the status.
if (!status.isComplete) {
status = state.status;
@@ -622,6 +623,7 @@
presubmitJob.endTime = state.endTime!;
presubmitJob.summary = state.summary;
presubmitJob.buildNumber = state.buildNumber;
+ presubmitJob.buildId = state.buildId;
} else {
status = state.status;
valid = true;
diff --git a/app_dart/test/model/common/presubmit_check_state_test.dart b/app_dart/test/model/common/presubmit_check_state_test.dart
index 9285c07..ea337af 100644
--- a/app_dart/test/model/common/presubmit_check_state_test.dart
+++ b/app_dart/test/model/common/presubmit_check_state_test.dart
@@ -14,6 +14,7 @@
group('PresubmitJobState', () {
test('BuildToPresubmitJobState extension maps build number', () {
final build = bbv2.Build(
+ id: Int64(67890),
builder: bbv2.BuilderID(builder: 'linux_test'),
status: bbv2.Status.SUCCESS,
number: 12345,
@@ -27,6 +28,7 @@
expect(state.jobName, 'linux_test');
expect(state.status, TaskStatus.succeeded);
expect(state.buildNumber, 12345);
+ expect(state.buildId, 67890);
});
});
}
diff --git a/app_dart/test/model/common/presubmit_completed_check_test.dart b/app_dart/test/model/common/presubmit_completed_check_test.dart
index ad9a33a..f5d53a2 100644
--- a/app_dart/test/model/common/presubmit_completed_check_test.dart
+++ b/app_dart/test/model/common/presubmit_completed_check_test.dart
@@ -11,6 +11,7 @@
import 'package:cocoon_service/src/model/github/checks.dart' as cocoon_checks;
import 'package:cocoon_service/src/service/config.dart';
import 'package:cocoon_service/src/service/luci_build_service/user_data.dart';
+import 'package:fixnum/fixnum.dart';
import 'package:github/github.dart';
import 'package:test/test.dart';
@@ -45,6 +46,7 @@
test('fromBuild creates correct unified check', () {
final build = Build(
+ id: Int64(98765),
builder: BuilderID(builder: 'test_builder'),
status: Status.SUCCESS,
);
@@ -74,10 +76,12 @@
expect(check.isUnifiedCheckRun, true);
expect(check.checkRun.name, Config.kFlutterPresubmitsName);
expect(check.buildNumber, 0);
+ expect(check.buildId, 98765);
});
test('fromBuild creates correct legacy check', () {
final build = Build(
+ id: Int64(98765),
builder: BuilderID(builder: 'test_builder'),
status: Status.SUCCESS,
number: 1234,
@@ -108,6 +112,7 @@
expect(check.isUnifiedCheckRun, false);
expect(check.checkRun.name, 'test_builder');
expect(check.buildNumber, 1234);
+ expect(check.buildId, 98765);
});
});
}
diff --git a/app_dart/test/model/firestore/presubmit_check_test.dart b/app_dart/test/model/firestore/presubmit_check_test.dart
index 9431764..0515161 100644
--- a/app_dart/test/model/firestore/presubmit_check_test.dart
+++ b/app_dart/test/model/firestore/presubmit_check_test.dart
@@ -100,6 +100,7 @@
expect(check.attemptNumber, 1);
expect(check.status, TaskStatus.waitingForBackfill);
expect(check.buildNumber, isNull);
+ expect(check.buildId, isNull);
expect(check.startTime, isNull);
expect(check.endTime, isNull);
expect(check.summary, isNull);
@@ -131,6 +132,7 @@
attemptNumber: 1,
creationTime: 1000,
buildNumber: 456,
+ buildId: 789,
startTime: 2000,
endTime: 3000,
summary: 'Success',
@@ -168,6 +170,7 @@
expect(loadedCheck.attemptNumber, 1);
expect(loadedCheck.creationTime, 1000);
expect(loadedCheck.buildNumber, 456);
+ expect(loadedCheck.buildId, 789);
expect(loadedCheck.startTime, 2000);
expect(loadedCheck.endTime, 3000);
expect(loadedCheck.summary, 'Success');
@@ -182,6 +185,7 @@
);
final build = bbv2.Build(
+ id: Int64(789),
number: 456,
createTime: bbv2.Timestamp(seconds: Int64(2000)),
startTime: bbv2.Timestamp(seconds: Int64(2100)),
@@ -192,6 +196,7 @@
check.updateFromBuild(build);
expect(check.buildNumber, 456);
+ expect(check.buildId, 789);
expect(check.creationTime, 2000000); // seconds to millis
expect(check.startTime, 2100000);
expect(check.endTime, 2200000);
@@ -232,5 +237,20 @@
check.buildNumber = null;
expect(check.buildNumber, isNull);
});
+
+ test('buildId setter updates fields', () {
+ final check = PresubmitJob.init(
+ slug: slug,
+ jobName: 'linux',
+ checkRunId: 123,
+ creationTime: 1000,
+ );
+
+ check.buildId = 789;
+ expect(check.buildId, 789);
+
+ check.buildId = null;
+ expect(check.buildId, isNull);
+ });
});
}
diff --git a/app_dart/test/request_handlers/get_presubmit_checks_test.dart b/app_dart/test/request_handlers/get_presubmit_checks_test.dart
index 4206090..b9712c0 100644
--- a/app_dart/test/request_handlers/get_presubmit_checks_test.dart
+++ b/app_dart/test/request_handlers/get_presubmit_checks_test.dart
@@ -83,6 +83,7 @@
endTime: 120,
summary: 'all good',
buildNumber: 456,
+ buildId: 98765,
);
await firestoreService.writeViaTransaction(
documentsToWrites([job], exists: false),
@@ -100,6 +101,7 @@
expect(jobs[0].jobName, 'linux');
expect(jobs[0].status, TaskStatus.succeeded);
expect(jobs[0].buildNumber, 456);
+ expect(jobs[0].buildId, 98765);
});
test('returns checks when found with owner and repo', () async {
diff --git a/app_dart/test/service/config_test.dart b/app_dart/test/service/config_test.dart
index 8fe7f3f..6763c83 100644
--- a/app_dart/test/service/config_test.dart
+++ b/app_dart/test/service/config_test.dart
@@ -55,6 +55,19 @@
expect(githubToken, 'githubToken');
});
+ test('geminiLogAnalyzerKey pulls from cache', () async {
+ const secretValue = 'my-gemini-key';
+ final cachedValue = Uint8List.fromList(secretValue.codeUnits);
+ await cacheService.set(
+ Config.configCacheName,
+ 'APP_DART_GEMINI_LOG_ANALYZER_KEY',
+ cachedValue,
+ );
+
+ final key = await config.geminiLogAnalyzerKey;
+ expect(key, equals('my-gemini-key'));
+ });
+
test('Returns the right flutter gold alert', () {
expect(
config.flutterGoldAlertConstant(RepositorySlug.full('flutter/flutter')),
diff --git a/app_dart/test/service/firestore/unified_check_run_test.dart b/app_dart/test/service/firestore/unified_check_run_test.dart
index d47b484..38bc618 100644
--- a/app_dart/test/service/firestore/unified_check_run_test.dart
+++ b/app_dart/test/service/firestore/unified_check_run_test.dart
@@ -176,6 +176,7 @@
startTime: 2000,
endTime: 3000,
buildNumber: 456,
+ buildId: 98765,
);
final result = await UnifiedCheckRun.markConclusion(
@@ -200,6 +201,7 @@
expect(checkDoc.status, TaskStatus.succeeded);
expect(checkDoc.endTime, 3000);
expect(checkDoc.buildNumber, 456);
+ expect(checkDoc.buildId, 98765);
});
test(
@@ -297,6 +299,7 @@
attemptNumber: 1,
startTime: 2000,
buildNumber: 456,
+ buildId: 98765,
);
final result = await UnifiedCheckRun.markConclusion(
@@ -319,6 +322,7 @@
expect(checkDoc.status, TaskStatus.inProgress);
expect(checkDoc.startTime, 2000);
expect(checkDoc.buildNumber, 456);
+ expect(checkDoc.buildId, 98765);
});
});
group('reInitializeFailedChecks', () {
diff --git a/packages/cocoon_common/lib/src/rpc_model/presubmit_job_response.dart b/packages/cocoon_common/lib/src/rpc_model/presubmit_job_response.dart
index a9543c2..7f62882 100644
--- a/packages/cocoon_common/lib/src/rpc_model/presubmit_job_response.dart
+++ b/packages/cocoon_common/lib/src/rpc_model/presubmit_job_response.dart
@@ -28,6 +28,7 @@
required this.status,
this.summary,
this.buildNumber,
+ this.buildId,
});
/// Creates a [PresubmitJobResponse] from [json] representation.
@@ -63,6 +64,9 @@
/// The LUCI build number.
final int? buildNumber;
+ /// The LUCI build ID.
+ final int? buildId;
+
@override
Map<String, Object?> toJson() => _$PresubmitJobResponseToJson(this);
}
diff --git a/packages/cocoon_common/lib/src/rpc_model/presubmit_job_response.g.dart b/packages/cocoon_common/lib/src/rpc_model/presubmit_job_response.g.dart
index c26621f..a904a92 100644
--- a/packages/cocoon_common/lib/src/rpc_model/presubmit_job_response.g.dart
+++ b/packages/cocoon_common/lib/src/rpc_model/presubmit_job_response.g.dart
@@ -27,6 +27,7 @@
),
summary: $checkedConvert('summary', (v) => v as String?),
buildNumber: $checkedConvert('build_number', (v) => (v as num?)?.toInt()),
+ buildId: $checkedConvert('build_id', (v) => (v as num?)?.toInt()),
);
return val;
},
@@ -37,6 +38,7 @@
'startTime': 'start_time',
'endTime': 'end_time',
'buildNumber': 'build_number',
+ 'buildId': 'build_id',
},
);
@@ -51,6 +53,7 @@
'status': instance.status,
'summary': ?instance.summary,
'build_number': ?instance.buildNumber,
+ 'build_id': ?instance.buildId,
};
const _$TaskStatusEnumMap = {
diff --git a/packages/cocoon_integration_test/lib/src/fakes/fake_config.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_config.dart
index 8213e39..a68e531 100644
--- a/packages/cocoon_integration_test/lib/src/fakes/fake_config.dart
+++ b/packages/cocoon_integration_test/lib/src/fakes/fake_config.dart
@@ -23,6 +23,7 @@
this.maxFilesChangedForSkippingEnginePhaseValue,
this.oauthClientIdValue,
this.githubOAuthTokenValue,
+ this.geminiLogAnalyzerKeyValue,
this.mergeConflictPullRequestMessageValue =
'default mergeConflictPullRequestMessageValue',
this.missingTestsPullRequestMessageValue =
@@ -66,6 +67,7 @@
int? batchSizeValue;
String? oauthClientIdValue;
String? githubOAuthTokenValue;
+ String? geminiLogAnalyzerKeyValue;
String mergeConflictPullRequestMessageValue;
String missingTestsPullRequestMessageValue;
String? wrongBaseBranchPullRequestMessageValue;
@@ -177,6 +179,10 @@
Future<String> get githubOAuthToken async => githubOAuthTokenValue ?? 'token';
@override
+ Future<String> get geminiLogAnalyzerKey async =>
+ geminiLogAnalyzerKeyValue ?? 'fake-gemini-key';
+
+ @override
String get mergeConflictPullRequestMessage =>
mergeConflictPullRequestMessageValue;