diff --git a/CODEOWNERS b/CODEOWNERS index 8bfc94786..65a0afadc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -11,6 +11,7 @@ app_dart/lib/src/request_handlers/dart_internal_subscription.dart @dre app_dart/lib/src/request_handlers/file_flaky_issue_and_pr.dart @keyonghan app_dart/lib/src/request_handlers/flush_cache.dart @keyonghan app_dart/lib/src/request_handlers/get_build_status.dart @keyonghan +app_dart/lib/src/request_handlers/get_build_status_badge.dart @CaseyHillers app_dart/lib/src/request_handlers/get_release_branches.dart @CaseyHillers app_dart/lib/src/request_handlers/get_repos.dart @keyonghan app_dart/lib/src/request_handlers/get_status.dart @keyonghan diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3bf36c821..6f7d45177 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,6 +18,12 @@ us first through the issue tracker with your idea so that we can help out and possibly guide you. Coordinating up front makes it much easier to avoid frustration later on. +### Implementations + +1. For existing functionality contributions, please loop corresponding CODEOWNERs in to make sure they know and can provide a review before a merge to avoid potential regressions. + +2. For new functionality like an app, please add your application as a top level folder and yourself as the point of contact in the Cocoon CODEOWNERS file. One good example of an existing app is `autosubmit`. Feel free to open an infra bug for any design/support discussions, as we are willing to help out. + ### Code reviews All submissions, including submissions by project members, require review. diff --git a/README.md b/README.md index 07eb0f8fd..9999e92e2 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ +[![Flutter CI Status](https://flutter-dashboard.appspot.com/api/public/build-status-badge?repo=cocooon)](https://flutter-dashboard.appspot.com/#/build?repo=cocoon&branch=main) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/flutter/cocoon/badge)](https://api.securityscorecards.dev/projects/github.com/flutter/cocoon) [![SLSA 3](https://slsa.dev/images/gh-badge-level3.svg)](https://slsa.dev) @@ -70,24 +71,14 @@ All the commands in this section assume that you are in the ### Running a local dev server -**This is for legacy users who were granted old security keys. Due to overground, this is no longer supported.** - ```sh -$ export GOOGLE_CLOUD_PROJECT=flutter-dashboard-dev # or flutter-dashboard for prod data -$ export GCLOUD_KEY=#your_secret # Required for reading/writing from Google Cloud -$ export COCOON_USE_IN_MEMORY_CACHE=true # Use an in memory cache locally instead of redis to prevent corruption -$ dart bin/server.dart +dart bin/local_server.dart ``` + This will output `Serving requests at 0.0.0.0:8080` indicating the server is working. New requests will be logged to the console. -To develop and test some features, you need to have a local service -account(key.json) with access to the project you will be connecting to. - -If you work for Google you can use the key with flutter-dashboard project -via [internal doc](https://g3doc.corp.google.com/company/teams/flutter/infrastructure/cocoon/local_development.md?cl=head#test-with-flutter-dashboard-dev-project). - ### Deploying a test version on Google Cloud To run live tests, build the app, and provide instructions for deploying to diff --git a/analyze/pubspec.yaml b/analyze/pubspec.yaml index 9838bbc3c..db48416ca 100644 --- a/analyze/pubspec.yaml +++ b/analyze/pubspec.yaml @@ -12,4 +12,4 @@ dependencies: dev_dependencies: mockito: 5.4.4 - test_api: 0.6.1 + test_api: 0.7.0 diff --git a/app_dart/Dockerfile b/app_dart/Dockerfile index d8a40648c..4814d8d53 100644 --- a/app_dart/Dockerfile +++ b/app_dart/Dockerfile @@ -4,7 +4,7 @@ # Dart Docker official images can be found here: https://hub.docker.com/_/dart -FROM dart:beta@sha256:e05aaad0f71e9d045e2a45b63a65f85e5e466b1cc1de34e7e684eede45a29d9b +FROM dart:beta@sha256:4bddd6ccaed32d2d2b8c27bcd71b1ebca13cec47f3abe7b12f640f9432da8e83 WORKDIR /app @@ -14,4 +14,4 @@ RUN dart pub get # Start server. EXPOSE 8080 -CMD ["/usr/lib/dart/bin/dart", "/app/bin/server.dart"] +CMD ["/usr/lib/dart/bin/dart", "/app/bin/gae_server.dart"] diff --git a/app_dart/README.md b/app_dart/README.md index d135d79b2..30cb9cfa0 100644 --- a/app_dart/README.md +++ b/app_dart/README.md @@ -11,7 +11,7 @@ This folder contains a Dart based backend for Cocoon. and authenticate yourself by running: ```sh -gcloud auth login +gcloud auth application-default login gcloud init ``` * [Install Flutter](https://flutter.dev/docs/get-started/install ) @@ -82,20 +82,10 @@ $ gcloud datastore indexes create index.yaml #### Using physical machine -* Setting up the environment - -```sh -export COCOON_USE_IN_MEMORY_CACHE=true -``` - -This environment is needed as you don't have access to the remote redis -instance during local development. - * Starting server ```sh -export COCOON_USE_IN_MEMORY_CACHE=true -dart bin/server.dart +dart bin/local_server.dart ``` If you see Serving requests at 0.0.0.0:8080 the dev server is working. diff --git a/app_dart/bin/gae_server.dart b/app_dart/bin/gae_server.dart new file mode 100644 index 000000000..378f1f255 --- /dev/null +++ b/app_dart/bin/gae_server.dart @@ -0,0 +1,78 @@ +// Copyright 2019 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:io'; + +import 'package:appengine/appengine.dart'; +import 'package:cocoon_service/cocoon_service.dart'; +import 'package:cocoon_service/server.dart'; +import 'package:cocoon_service/src/service/commit_service.dart'; +import 'package:gcloud/db.dart'; + +Future main() async { + await withAppEngineServices(() async { + useLoggingPackageAdaptor(); + + final CacheService cache = CacheService(inMemory: false); + final Config config = Config(dbService, cache); + final AuthenticationProvider authProvider = AuthenticationProvider(config: config); + final AuthenticationProvider swarmingAuthProvider = SwarmingAuthenticationProvider(config: config); + final BuildBucketClient buildBucketClient = BuildBucketClient( + accessTokenService: AccessTokenService.defaultProvider(config), + ); + + /// LUCI service class to communicate with buildBucket service. + final LuciBuildService luciBuildService = LuciBuildService( + config: config, + cache: cache, + buildBucketClient: buildBucketClient, + pubsub: const PubSub(), + ); + + /// Github checks api service used to provide luci test execution status on the Github UI. + final GithubChecksService githubChecksService = GithubChecksService( + config, + ); + + // Gerrit service class to communicate with GoB. + final GerritService gerritService = GerritService(config: config); + + /// Cocoon scheduler service to manage validating commits in presubmit and postsubmit. + final Scheduler scheduler = Scheduler( + cache: cache, + config: config, + githubChecksService: githubChecksService, + luciBuildService: luciBuildService, + ); + + final BranchService branchService = BranchService( + config: config, + gerritService: gerritService, + ); + + final CommitService commitService = CommitService(config: config); + + final Server server = createServer( + config: config, + cache: cache, + authProvider: authProvider, + branchService: branchService, + buildBucketClient: buildBucketClient, + gerritService: gerritService, + scheduler: scheduler, + luciBuildService: luciBuildService, + githubChecksService: githubChecksService, + commitService: commitService, + swarmingAuthProvider: swarmingAuthProvider, + ); + + return runAppEngine( + server, + onAcceptingConnections: (InternetAddress address, int port) { + final String host = address.isLoopback ? 'localhost' : address.host; + print('Serving requests at http://$host:$port/'); + }, + ); + }); +} diff --git a/app_dart/bin/local_server.dart b/app_dart/bin/local_server.dart new file mode 100644 index 000000000..0bd8bc1c0 --- /dev/null +++ b/app_dart/bin/local_server.dart @@ -0,0 +1,84 @@ +// Copyright 2019 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:io'; + +import 'package:appengine/appengine.dart'; +import 'package:cocoon_service/cocoon_service.dart'; +import 'package:cocoon_service/server.dart'; +import 'package:cocoon_service/src/model/appengine/cocoon_config.dart'; +import 'package:cocoon_service/src/service/commit_service.dart'; +import 'package:cocoon_service/src/service/datastore.dart'; +import 'package:gcloud/db.dart'; + +import '../test/src/datastore/fake_datastore.dart'; + +Future main() async { + final CacheService cache = CacheService(inMemory: false); + final DatastoreDB dbService = FakeDatastoreDB(); + final DatastoreService datastoreService = DatastoreService(dbService, defaultMaxEntityGroups); + await datastoreService.insert([ + CocoonConfig.fake(dbService.emptyKey.append(CocoonConfig, id: 'WebhookKey'), 'fake-secret'), + CocoonConfig.fake(dbService.emptyKey.append(CocoonConfig, id: 'FrobWebhookKey'), 'fake-secret'), + ]); + final Config config = Config(dbService, cache); + final AuthenticationProvider authProvider = AuthenticationProvider(config: config); + final AuthenticationProvider swarmingAuthProvider = SwarmingAuthenticationProvider(config: config); + final BuildBucketClient buildBucketClient = BuildBucketClient( + accessTokenService: AccessTokenService.defaultProvider(config), + ); + + /// LUCI service class to communicate with buildBucket service. + final LuciBuildService luciBuildService = LuciBuildService( + config: config, + cache: cache, + buildBucketClient: buildBucketClient, + pubsub: const PubSub(), + ); + + /// Github checks api service used to provide luci test execution status on the Github UI. + final GithubChecksService githubChecksService = GithubChecksService( + config, + ); + + // Gerrit service class to communicate with GoB. + final GerritService gerritService = GerritService(config: config); + + /// Cocoon scheduler service to manage validating commits in presubmit and postsubmit. + final Scheduler scheduler = Scheduler( + cache: cache, + config: config, + githubChecksService: githubChecksService, + luciBuildService: luciBuildService, + ); + + final BranchService branchService = BranchService( + config: config, + gerritService: gerritService, + ); + + final CommitService commitService = CommitService(config: config); + + final Server server = createServer( + config: config, + cache: cache, + authProvider: authProvider, + branchService: branchService, + buildBucketClient: buildBucketClient, + gerritService: gerritService, + scheduler: scheduler, + luciBuildService: luciBuildService, + githubChecksService: githubChecksService, + commitService: commitService, + swarmingAuthProvider: swarmingAuthProvider, + ); + + return runAppEngine( + server, + onAcceptingConnections: (InternetAddress address, int port) { + final String host = address.isLoopback ? 'localhost' : address.host; + print('Serving requests at http://$host:$port/'); + }, + ); +} diff --git a/app_dart/bin/server.dart b/app_dart/bin/server.dart deleted file mode 100644 index e278599e5..000000000 --- a/app_dart/bin/server.dart +++ /dev/null @@ -1,320 +0,0 @@ -// Copyright 2019 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:io'; -import 'dart:math'; - -import 'package:appengine/appengine.dart'; -import 'package:cocoon_service/cocoon_service.dart'; -import 'package:cocoon_service/src/service/commit_service.dart'; -import 'package:gcloud/db.dart'; - -/// For local development, you might want to set this to true. -const String _kCocoonUseInMemoryCache = 'COCOON_USE_IN_MEMORY_CACHE'; - -Future main() async { - await withAppEngineServices(() async { - useLoggingPackageAdaptor(); - - final bool inMemoryCache = Platform.environment[_kCocoonUseInMemoryCache] == 'true'; - final CacheService cache = CacheService(inMemory: inMemoryCache); - - final Config config = Config(dbService, cache); - final AuthenticationProvider authProvider = AuthenticationProvider(config: config); - final AuthenticationProvider swarmingAuthProvider = SwarmingAuthenticationProvider(config: config); - final BuildBucketClient buildBucketClient = BuildBucketClient( - accessTokenService: AccessTokenService.defaultProvider(config), - ); - - /// LUCI service class to communicate with buildBucket service. - final LuciBuildService luciBuildService = LuciBuildService( - config: config, - cache: cache, - buildBucketClient: buildBucketClient, - pubsub: const PubSub(), - ); - - /// Github checks api service used to provide luci test execution status on the Github UI. - final GithubChecksService githubChecksService = GithubChecksService( - config, - ); - - // Gerrit service class to communicate with GoB. - final GerritService gerritService = GerritService(config: config); - - /// Cocoon scheduler service to manage validating commits in presubmit and postsubmit. - final Scheduler scheduler = Scheduler( - cache: cache, - config: config, - githubChecksService: githubChecksService, - luciBuildService: luciBuildService, - ); - - final BranchService branchService = BranchService( - config: config, - gerritService: gerritService, - ); - - final CommitService commitService = CommitService(config: config); - - final Map> handlers = >{ - '/api/check_flaky_builders': CheckFlakyBuilders( - config: config, - authenticationProvider: authProvider, - ), - '/api/create-branch': CreateBranch( - branchService: branchService, - config: config, - authenticationProvider: authProvider, - ), - '/api/dart-internal-subscription': DartInternalSubscription( - cache: cache, - config: config, - buildBucketClient: buildBucketClient, - ), - '/api/file_flaky_issue_and_pr': FileFlakyIssueAndPR( - config: config, - authenticationProvider: authProvider, - ), - '/api/flush-cache': FlushCache( - config: config, - authenticationProvider: authProvider, - cache: cache, - ), - '/api/github-webhook-pullrequest': GithubWebhook( - config: config, - pubsub: const PubSub(), - secret: config.webhookKey, - topic: 'github-webhooks', - ), - // TODO(chillers): Move to release service. https://github.com/flutter/flutter/issues/132082 - '/api/github/frob-webhook': GithubWebhook( - config: config, - pubsub: const PubSub(), - secret: config.frobWebhookKey, - topic: 'frob-webhooks', - ), - '/api/github/webhook-subscription': GithubWebhookSubscription( - config: config, - cache: cache, - gerritService: gerritService, - githubChecksService: githubChecksService, - scheduler: scheduler, - commitService: commitService, - ), - '/api/presubmit-luci-subscription': PresubmitLuciSubscription( - cache: cache, - config: config, - buildBucketClient: buildBucketClient, - luciBuildService: luciBuildService, - githubChecksService: githubChecksService, - scheduler: scheduler, - ), - '/api/postsubmit-luci-subscription': PostsubmitLuciSubscription( - cache: cache, - config: config, - scheduler: scheduler, - githubChecksService: githubChecksService, - ), - '/api/push-build-status-to-github': PushBuildStatusToGithub( - config: config, - authenticationProvider: authProvider, - ), - '/api/push-gold-status-to-github': PushGoldStatusToGithub( - config: config, - authenticationProvider: authProvider, - ), - '/api/reset-prod-task': ResetProdTask( - config: config, - authenticationProvider: authProvider, - luciBuildService: luciBuildService, - scheduler: scheduler, - ), - '/api/reset-try-task': ResetTryTask( - config: config, - authenticationProvider: authProvider, - scheduler: scheduler, - ), - '/api/scheduler/batch-backfiller': BatchBackfiller( - config: config, - scheduler: scheduler, - ), - '/api/scheduler/batch-request-subscription': SchedulerRequestSubscription( - cache: cache, - config: config, - buildBucketClient: buildBucketClient, - ), - '/api/scheduler/vacuum-stale-tasks': VacuumStaleTasks( - config: config, - ), - '/api/update_existing_flaky_issues': UpdateExistingFlakyIssue( - config: config, - authenticationProvider: authProvider, - ), - - /// Updates task related details. - /// - /// This API updates task status in datastore and - /// pushes performance metrics to skia-perf. - /// - /// POST: /api-update-status - /// - /// Parameters: - /// CommitBranch: (string in body). Branch of commit. - /// CommitSha: (string in body). Sha of commit. - /// BuilderName: (string in body). Name of the luci builder. - /// NewStatus: (string in body) required. Status of the task. - /// ResultData: (string in body) optional. Benchmark data. - /// BenchmarkScoreKeys: (string in body) optional. Benchmark data. - /// - /// Response: Status 200 OK - '/api/update-task-status': UpdateTaskStatus( - config: config, - authenticationProvider: swarmingAuthProvider, - ), - '/api/vacuum-github-commits': VacuumGithubCommits( - config: config, - authenticationProvider: authProvider, - scheduler: scheduler, - ), - - /// Returns status of the framework tree. - /// - /// Returns serialized proto with enum representing the - /// status of the tree and list of offending tasks. - /// - /// GET: /api/public/build-status - /// - /// Parameters: - /// branch: (string in query) default: 'master'. Name of the repo branch. - /// - /// Response: Status 200 OK - /// Returns [BuildStatusResponse]: - /// { - /// 1: 2, - /// 2: [ "win_tool_tests_commands", "win_build_test", "win_module_test"] - /// } - '/api/public/build-status': CacheRequestHandler( - cache: cache, - config: config, - delegate: GetBuildStatus(config: config), - ttl: const Duration(seconds: 15), - ), - '/api/public/get-release-branches': CacheRequestHandler( - cache: cache, - config: config, - delegate: GetReleaseBranches(config: config, branchService: branchService), - ttl: const Duration(hours: 1), - ), - - /// Returns task results for commits. - /// - /// Returns result details about each task in each checklist for every commit. - /// - /// GET: /api/public/get-status - /// - /// Parameters: - /// branch: (string in query) default: 'master'. Name of the repo branch. - /// lastCommitKey: (string in query) optional. Encoded commit key for the last commit to return resutls. - /// - /// Response: Status: 200 OK - /// {"Statuses":[ - /// {"Checklist":{ - /// "Key":"ah..jgM", - /// "Checklist":{"FlutterRepositoryPath":"flutter/flutter", - /// "CreateTimestamp":1620134239000, - /// "Commit":{"Sha":"7f1d1414cc5f0b0317272ced49a9c0b44e5c3af8", - /// "Message":"Revert \"Migrate to ChannelBuffers.push\"", - /// "Author":{"Login":"renyou","avatar_url":"https://avatars.githubusercontent.com/u/666474?v=4"}},"Branch":"master"}}, - /// "Stages":[{"Name":"chromebot", - /// "Tasks":[ - /// {"Task":{ - /// "ChecklistKey":"ahF..jgM", - /// "CreateTimestamp":1620134239000, - /// "StartTimestamp":0, - /// "EndTimestamp":1620136203757, - /// "Name":"linux_cubic_bezier_perf__e2e_summary", - /// "Attempts":1, - /// "Flaky":false, - /// "TimeoutInMinutes":0, - /// "Reason":"", - /// "BuildNumber":null, - /// "BuildNumberList":"1279", - /// "BuilderName":"Linux cubic_bezier_perf__e2e_summary", - /// "luciBucket":"luci.flutter.prod", - /// "RequiredCapabilities":["can-update-github"], - /// "ReservedForAgentID":"", - /// "StageName":"chromebot", - /// "Status":"Succeeded" - /// }, - /// ], - /// "Status": "InProgress", - /// ]}, - /// }, - /// } - '/api/public/get-status': CacheRequestHandler( - cache: cache, - config: config, - delegate: GetStatus(config: config), - ), - - '/api/public/get-green-commits': GetGreenCommits(config: config), - - /// Record GitHub API quota usage in BigQuery. - /// - /// Pushes data to BigQuery for metric collection to - /// analyze usage over time. - /// - /// This api is called via cron job. - /// - /// GET: /api/public/github-rate-limit-status - /// - /// Response: Status 200 OK - '/api/public/github-rate-limit-status': CacheRequestHandler( - config: config, - cache: cache, - ttl: const Duration(minutes: 1), - delegate: GithubRateLimitStatus(config: config), - ), - '/api/public/repos': GetRepos(config: config), - - /// Handler for AppEngine to identify when dart server is ready to serve requests. - '/readiness_check': ReadinessCheck(config: config), - }; - - return runAppEngine( - (HttpRequest request) async { - if (handlers.containsKey(request.uri.path)) { - final RequestHandler handler = handlers[request.uri.path]!; - await handler.service(request); - } else { - /// Requests with query parameters and anchors need to be trimmed to get the file path. - // TODO(chillers): Use toFilePath(), https://github.com/dart-lang/sdk/issues/39373 - final int queryIndex = - request.uri.path.contains('?') ? request.uri.path.indexOf('?') : request.uri.path.length; - final int anchorIndex = - request.uri.path.contains('#') ? request.uri.path.indexOf('#') : request.uri.path.length; - - /// Trim to the first instance of an anchor or query. - final int trimIndex = min(queryIndex, anchorIndex); - final String filePath = request.uri.path.substring(0, trimIndex); - - const Map redirects = { - '/build.html': '/#/build', - }; - if (redirects.containsKey(filePath)) { - request.response.statusCode = HttpStatus.permanentRedirect; - return request.response.redirect(Uri.parse(redirects[filePath]!)); - } - - await StaticFileHandler(filePath, config: config).service(request); - } - }, - onAcceptingConnections: (InternetAddress address, int port) { - final String host = address.isLoopback ? 'localhost' : address.host; - print('Serving requests at http://$host:$port/'); - }, - ); - }); -} diff --git a/app_dart/lib/cocoon_service.dart b/app_dart/lib/cocoon_service.dart index d0137b3ef..69a336b59 100644 --- a/app_dart/lib/cocoon_service.dart +++ b/app_dart/lib/cocoon_service.dart @@ -10,6 +10,7 @@ export 'src/request_handlers/dart_internal_subscription.dart'; export 'src/request_handlers/file_flaky_issue_and_pr.dart'; export 'src/request_handlers/flush_cache.dart'; export 'src/request_handlers/get_build_status.dart'; +export 'src/request_handlers/get_build_status_badge.dart'; export 'src/request_handlers/get_release_branches.dart'; export 'src/request_handlers/get_repos.dart'; export 'src/request_handlers/get_status.dart'; diff --git a/app_dart/lib/server.dart b/app_dart/lib/server.dart new file mode 100644 index 000000000..5aa02cfb7 --- /dev/null +++ b/app_dart/lib/server.dart @@ -0,0 +1,282 @@ +// Copyright 2019 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:io'; +import 'dart:math'; + +import 'package:cocoon_service/cocoon_service.dart'; +import 'package:cocoon_service/src/service/commit_service.dart'; + +typedef Server = Future Function(HttpRequest); + +/// Creates a service with the given dependencies. +Server createServer({ + required Config config, + required CacheService cache, + required AuthenticationProvider authProvider, + required AuthenticationProvider swarmingAuthProvider, + required BranchService branchService, + required BuildBucketClient buildBucketClient, + required LuciBuildService luciBuildService, + required GithubChecksService githubChecksService, + required CommitService commitService, + required GerritService gerritService, + required Scheduler scheduler, +}) { + final Map> handlers = >{ + '/api/check_flaky_builders': CheckFlakyBuilders( + config: config, + authenticationProvider: authProvider, + ), + '/api/create-branch': CreateBranch( + branchService: branchService, + config: config, + authenticationProvider: authProvider, + ), + '/api/dart-internal-subscription': DartInternalSubscription( + cache: cache, + config: config, + buildBucketClient: buildBucketClient, + ), + '/api/file_flaky_issue_and_pr': FileFlakyIssueAndPR( + config: config, + authenticationProvider: authProvider, + ), + '/api/flush-cache': FlushCache( + config: config, + authenticationProvider: authProvider, + cache: cache, + ), + '/api/github-webhook-pullrequest': GithubWebhook( + config: config, + pubsub: const PubSub(), + secret: config.webhookKey, + topic: 'github-webhooks', + ), + // TODO(chillers): Move to release service. https://github.com/flutter/flutter/issues/132082 + '/api/github/frob-webhook': GithubWebhook( + config: config, + pubsub: const PubSub(), + secret: config.frobWebhookKey, + topic: 'frob-webhooks', + ), + '/api/github/webhook-subscription': GithubWebhookSubscription( + config: config, + cache: cache, + gerritService: gerritService, + githubChecksService: githubChecksService, + scheduler: scheduler, + commitService: commitService, + ), + '/api/presubmit-luci-subscription': PresubmitLuciSubscription( + cache: cache, + config: config, + buildBucketClient: buildBucketClient, + luciBuildService: luciBuildService, + githubChecksService: githubChecksService, + scheduler: scheduler, + ), + '/api/postsubmit-luci-subscription': PostsubmitLuciSubscription( + cache: cache, + config: config, + scheduler: scheduler, + githubChecksService: githubChecksService, + ), + '/api/push-build-status-to-github': PushBuildStatusToGithub( + config: config, + authenticationProvider: authProvider, + ), + '/api/push-gold-status-to-github': PushGoldStatusToGithub( + config: config, + authenticationProvider: authProvider, + ), + '/api/reset-prod-task': ResetProdTask( + config: config, + authenticationProvider: authProvider, + luciBuildService: luciBuildService, + scheduler: scheduler, + ), + '/api/reset-try-task': ResetTryTask( + config: config, + authenticationProvider: authProvider, + scheduler: scheduler, + ), + '/api/scheduler/batch-backfiller': BatchBackfiller( + config: config, + scheduler: scheduler, + ), + '/api/scheduler/batch-request-subscription': SchedulerRequestSubscription( + cache: cache, + config: config, + buildBucketClient: buildBucketClient, + ), + '/api/scheduler/vacuum-stale-tasks': VacuumStaleTasks( + config: config, + ), + '/api/update_existing_flaky_issues': UpdateExistingFlakyIssue( + config: config, + authenticationProvider: authProvider, + ), + + /// Updates task related details. + /// + /// This API updates task status in datastore and + /// pushes performance metrics to skia-perf. + /// + /// POST: /api-update-status + /// + /// Parameters: + /// CommitBranch: (string in body). Branch of commit. + /// CommitSha: (string in body). Sha of commit. + /// BuilderName: (string in body). Name of the luci builder. + /// NewStatus: (string in body) required. Status of the task. + /// ResultData: (string in body) optional. Benchmark data. + /// BenchmarkScoreKeys: (string in body) optional. Benchmark data. + /// + /// Response: Status 200 OK + '/api/update-task-status': UpdateTaskStatus( + config: config, + authenticationProvider: swarmingAuthProvider, + ), + '/api/vacuum-github-commits': VacuumGithubCommits( + config: config, + authenticationProvider: authProvider, + scheduler: scheduler, + ), + + /// Returns status of the framework tree. + /// + /// Returns serialized proto with enum representing the + /// status of the tree and list of offending tasks. + /// + /// GET: /api/public/build-status + /// + /// Parameters: + /// branch: (string in query) default: 'master'. Name of the repo branch. + /// + /// Response: Status 200 OK + /// Returns [BuildStatusResponse]: + /// { + /// 1: 2, + /// 2: [ "win_tool_tests_commands", "win_build_test", "win_module_test"] + /// } + '/api/public/build-status': CacheRequestHandler( + cache: cache, + config: config, + delegate: GetBuildStatus(config: config), + ttl: const Duration(seconds: 15), + ), + '/api/public/build-status-badge': CacheRequestHandler( + cache: cache, + config: config, + delegate: GetBuildStatusBadge(config: config), + ttl: const Duration(seconds: 15), + ), + '/api/public/get-release-branches': CacheRequestHandler( + cache: cache, + config: config, + delegate: GetReleaseBranches(config: config, branchService: branchService), + ttl: const Duration(hours: 1), + ), + + /// Returns task results for commits. + /// + /// Returns result details about each task in each checklist for every commit. + /// + /// GET: /api/public/get-status + /// + /// Parameters: + /// branch: (string in query) default: 'master'. Name of the repo branch. + /// lastCommitKey: (string in query) optional. Encoded commit key for the last commit to return resutls. + /// + /// Response: Status: 200 OK + /// {"Statuses":[ + /// {"Checklist":{ + /// "Key":"ah..jgM", + /// "Checklist":{"FlutterRepositoryPath":"flutter/flutter", + /// "CreateTimestamp":1620134239000, + /// "Commit":{"Sha":"7f1d1414cc5f0b0317272ced49a9c0b44e5c3af8", + /// "Message":"Revert \"Migrate to ChannelBuffers.push\"", + /// "Author":{"Login":"renyou","avatar_url":"https://avatars.githubusercontent.com/u/666474?v=4"}},"Branch":"master"}}, + /// "Stages":[{"Name":"chromebot", + /// "Tasks":[ + /// {"Task":{ + /// "ChecklistKey":"ahF..jgM", + /// "CreateTimestamp":1620134239000, + /// "StartTimestamp":0, + /// "EndTimestamp":1620136203757, + /// "Name":"linux_cubic_bezier_perf__e2e_summary", + /// "Attempts":1, + /// "Flaky":false, + /// "TimeoutInMinutes":0, + /// "Reason":"", + /// "BuildNumber":null, + /// "BuildNumberList":"1279", + /// "BuilderName":"Linux cubic_bezier_perf__e2e_summary", + /// "luciBucket":"luci.flutter.prod", + /// "RequiredCapabilities":["can-update-github"], + /// "ReservedForAgentID":"", + /// "StageName":"chromebot", + /// "Status":"Succeeded" + /// }, + /// ], + /// "Status": "InProgress", + /// ]}, + /// }, + /// } + '/api/public/get-status': CacheRequestHandler( + cache: cache, + config: config, + delegate: GetStatus(config: config), + ), + + '/api/public/get-green-commits': GetGreenCommits(config: config), + + /// Record GitHub API quota usage in BigQuery. + /// + /// Pushes data to BigQuery for metric collection to + /// analyze usage over time. + /// + /// This api is called via cron job. + /// + /// GET: /api/public/github-rate-limit-status + /// + /// Response: Status 200 OK + '/api/public/github-rate-limit-status': CacheRequestHandler( + config: config, + cache: cache, + ttl: const Duration(minutes: 1), + delegate: GithubRateLimitStatus(config: config), + ), + '/api/public/repos': GetRepos(config: config), + + /// Handler for AppEngine to identify when dart server is ready to serve requests. + '/readiness_check': ReadinessCheck(config: config), + }; + + return ((HttpRequest request) async { + if (handlers.containsKey(request.uri.path)) { + final RequestHandler handler = handlers[request.uri.path]!; + await handler.service(request); + } else { + /// Requests with query parameters and anchors need to be trimmed to get the file path. + // TODO(chillers): Use toFilePath(), https://github.com/dart-lang/sdk/issues/39373 + final int queryIndex = request.uri.path.contains('?') ? request.uri.path.indexOf('?') : request.uri.path.length; + final int anchorIndex = request.uri.path.contains('#') ? request.uri.path.indexOf('#') : request.uri.path.length; + + /// Trim to the first instance of an anchor or query. + final int trimIndex = min(queryIndex, anchorIndex); + final String filePath = request.uri.path.substring(0, trimIndex); + + const Map redirects = { + '/build.html': '/#/build', + }; + if (redirects.containsKey(filePath)) { + request.response.statusCode = HttpStatus.permanentRedirect; + return request.response.redirect(Uri.parse(redirects[filePath]!)); + } + await StaticFileHandler(filePath, config: config).service(request); + } + }); +} diff --git a/app_dart/lib/src/model/appengine/cocoon_config.dart b/app_dart/lib/src/model/appengine/cocoon_config.dart index 25705c175..ae898ca21 100644 --- a/app_dart/lib/src/model/appengine/cocoon_config.dart +++ b/app_dart/lib/src/model/appengine/cocoon_config.dart @@ -6,6 +6,14 @@ import 'package:gcloud/db.dart'; @Kind(name: 'CocoonConfig', idType: IdType.String) class CocoonConfig extends Model { + CocoonConfig(); + + /// Helper function only for local and test environments. + CocoonConfig.fake(Key? key, this.value) { + parentKey = key?.parent; + id = key?.id; + } + @StringProperty(propertyName: 'ParameterValue') late String value; } diff --git a/app_dart/lib/src/request_handlers/get_build_status.dart b/app_dart/lib/src/request_handlers/get_build_status.dart index e4348426e..024c671e2 100644 --- a/app_dart/lib/src/request_handlers/get_build_status.dart +++ b/app_dart/lib/src/request_handlers/get_build_status.dart @@ -28,15 +28,18 @@ class GetBuildStatus extends RequestHandler { @override Future get() async { + final BuildStatusResponse response = await createResponse(); + return Body.forJson(response.writeToJsonMap()); + } + + Future createResponse() async { final DatastoreService datastore = datastoreProvider(config.db); final BuildStatusService buildStatusService = buildStatusProvider(datastore); final String repoName = request!.uri.queryParameters[kRepoParam] ?? 'flutter'; final RepositorySlug slug = RepositorySlug('flutter', repoName); final BuildStatus status = (await buildStatusService.calculateCumulativeStatus(slug))!; - final BuildStatusResponse response = BuildStatusResponse() + return BuildStatusResponse() ..buildStatus = status.succeeded ? EnumBuildStatus.success : EnumBuildStatus.failure ..failingTasks.addAll(status.failedTasks); - - return Body.forJson(response.writeToJsonMap()); } } diff --git a/app_dart/lib/src/request_handlers/get_build_status_badge.dart b/app_dart/lib/src/request_handlers/get_build_status_badge.dart new file mode 100644 index 000000000..823cb6f59 --- /dev/null +++ b/app_dart/lib/src/request_handlers/get_build_status_badge.dart @@ -0,0 +1,63 @@ +// Copyright 2019 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:io'; + +import 'package:cocoon_service/protos.dart'; +import 'package:meta/meta.dart'; + +import '../request_handling/body.dart'; +import 'get_build_status.dart'; + +/// [GetBuildStatusBadge] returns an SVG representing the current tree status for the given repo. +/// +/// It reuses [GetBuildStatus] and translates it to the SVG. The primary caller for this is the +/// README's from the larger Flutter repositories. +@immutable +class GetBuildStatusBadge extends GetBuildStatus { + const GetBuildStatusBadge({ + required super.config, + @visibleForTesting super.datastoreProvider, + @visibleForTesting super.buildStatusProvider, + }); + + /// Provides a template that is easily injectable. + /// + /// Template follows the mustache format of `{{ VARIABLE }}`. + final String template = + ''' + Flutter CI: {{ STATUS }} + + + + + + + + + + Flutter CI + {{ STATUS }} + '''; + + static const red = '#e05d44'; + static const green = '#3BB143'; + + @override + Future get() async { + // Set HTTP content-type so SVG is viewable. + final HttpResponse response = request!.response; + response.headers.contentType = ContentType.parse('image/svg+xml'); + final BuildStatusResponse buildStatusResponse = await super.createResponse(); + return Body.forString(generateSVG(buildStatusResponse)); + } + + String generateSVG(BuildStatusResponse response) { + final bool passing = response.failingTasks.isEmpty; + return template + .replaceAll('{{ STATUS }}', passing ? 'passing' : '${response.failingTasks.length} failures') + .replaceAll('{{ COLOR }}', passing ? green : red); + } +} diff --git a/app_dart/lib/src/request_handlers/github/webhook_subscription.dart b/app_dart/lib/src/request_handlers/github/webhook_subscription.dart index dd5d75e70..2bfb3e0f7 100644 --- a/app_dart/lib/src/request_handlers/github/webhook_subscription.dart +++ b/app_dart/lib/src/request_handlers/github/webhook_subscription.dart @@ -34,8 +34,6 @@ Set kNeedsTests = { Config.packagesSlug, }; -final RegExp kEngineTestRegExp = RegExp(r'(tests?|benchmarks?)\.(dart|java|mm|m|cc|sh|py)$'); - // Extentions for files that use // for single line comments. // See [_allChangesAreCodeComments] for more. @visibleForTesting @@ -319,7 +317,7 @@ class GithubWebhookSubscription extends SubscriptionHandler { } // Check to see if tests were submitted with this PR. - if (_isATest(filename)) { + if (_isAFrameworkTest(filename)) { hasTests = true; } } @@ -346,7 +344,7 @@ class GithubWebhookSubscription extends SubscriptionHandler { } } - bool _isATest(String filename) { + bool _isAFrameworkTest(String filename) { if (kNotActuallyATest.any(filename.endsWith)) { return false; } @@ -412,7 +410,7 @@ class GithubWebhookSubscription extends SubscriptionHandler { needsTests = !_allChangesAreCodeComments(file); } - if (kEngineTestRegExp.hasMatch(filename.toLowerCase())) { + if (_isAnEngineTest(filename)) { hasTests = true; } } @@ -430,6 +428,14 @@ class GithubWebhookSubscription extends SubscriptionHandler { } } + bool _isAnEngineTest(String filename) { + final RegExp engineTestRegExp = RegExp(r'(tests?|benchmarks?)\.(dart|java|mm|m|cc|sh|py)$'); + return filename.contains('IosBenchmarks') || + filename.contains('IosUnitTests') || + filename.contains('scenario_app') || + engineTestRegExp.hasMatch(filename.toLowerCase()); + } + bool _fileContainsAddedCode(PullRequestFile file) { // When null, do not assume 0 lines have been added. final int linesAdded = file.additionsCount ?? 1; diff --git a/app_dart/lib/src/request_handlers/scheduler/vacuum_stale_tasks.dart b/app_dart/lib/src/request_handlers/scheduler/vacuum_stale_tasks.dart index 852c526b7..8a8e066c8 100644 --- a/app_dart/lib/src/request_handlers/scheduler/vacuum_stale_tasks.dart +++ b/app_dart/lib/src/request_handlers/scheduler/vacuum_stale_tasks.dart @@ -53,32 +53,16 @@ class VacuumStaleTasks extends RequestHandler { final DatastoreService datastore = datastoreProvider(config.db); final List tasks = await datastore.queryRecentTasks(slug: slug).toList(); - final Set tasksToBeReset = {}; + final List tasksToBeReset = []; for (FullTask fullTask in tasks) { final Task task = fullTask.task; - if (task.status != Task.statusInProgress) { - continue; - } - - if (task.createTimestamp == null) { - log.fine('Vacuuming $task due to createTimestamp being null'); + if (task.status == Task.statusInProgress && task.buildNumber == null) { + task.status = Task.statusNew; + task.createTimestamp = 0; tasksToBeReset.add(task); - continue; - } - - final DateTime now = nowValue ?? DateTime.now(); - final DateTime create = DateTime.fromMillisecondsSinceEpoch(task.createTimestamp!); - final Duration queueTime = now.difference(create); - - if (queueTime > kTimeoutLimit) { - log.fine('Vacuuming $task due to staleness'); - tasksToBeReset.add(task); - continue; } } - - final Iterable inserts = - tasksToBeReset.map((Task task) => task..status = Task.statusNew).map((Task task) => task..createTimestamp = 0); - await datastore.insert(inserts.toList()); + log.info('Vacuuming stale tasks: $tasksToBeReset'); + await datastore.insert(tasksToBeReset); } } diff --git a/app_dart/lib/src/service/config.dart b/app_dart/lib/src/service/config.dart index 84f288ff9..10dc0ff76 100644 --- a/app_dart/lib/src/service/config.dart +++ b/app_dart/lib/src/service/config.dart @@ -122,12 +122,10 @@ class Config { } Future _getValueFromDatastore(String id) async { - final CocoonConfig cocoonConfig = CocoonConfig() - ..id = id - ..parentKey = _db.emptyKey; - final CocoonConfig result = await _db.lookupValue(cocoonConfig.key); - - return Uint8List.fromList(result.value.codeUnits); + final CocoonConfig? result = await _db.lookupOrNull( + _db.emptyKey.append(CocoonConfig, id: id), + ); + return Uint8List.fromList(result!.value.codeUnits); } // GitHub App properties. @@ -227,7 +225,7 @@ class Config { 'request may not have tests. Please make sure to add tests before merging. ' 'If you need ' '[an exemption](https://github.com/flutter/flutter/wiki/Tree-hygiene#tests) ' - 'to this rule, contact "@text-exemption-reviewers" in the #hackers ' + 'to this rule, contact "@test-exemption-reviewer" in the #hackers ' 'channel in [Chat](https://github.com/flutter/flutter/wiki/Chat) ' '(don\'t just cc them here, they won\'t see it! Use Discord!).' '\n\n' diff --git a/app_dart/pubspec.yaml b/app_dart/pubspec.yaml index 69c6bc485..372da7714 100644 --- a/app_dart/pubspec.yaml +++ b/app_dart/pubspec.yaml @@ -24,7 +24,7 @@ dependencies: googleapis: 11.4.0 googleapis_auth: 1.4.1 gql: 1.0.1-alpha+1696717343881 - graphql: 5.2.0-beta.6 + graphql: 5.2.0-beta.7 grpc: 3.2.4 http: 1.1.2 json_annotation: 4.8.1 @@ -35,7 +35,7 @@ dependencies: neat_cache: 2.0.3 path: 1.9.0 process: 5.0.1 - process_runner: 4.1.4 + process_runner: 4.2.0 protobuf: 2.1.0 retry: ^3.1.2 truncate: 3.0.1 @@ -49,7 +49,7 @@ dev_dependencies: json_serializable: 6.7.1 mockito: 5.4.4 platform: 3.1.3 - test: 1.24.9 + test: 1.25.0 builders: json_serializable: 3.3.0 diff --git a/app_dart/test/request_handlers/get_build_status_badge_test.dart b/app_dart/test/request_handlers/get_build_status_badge_test.dart new file mode 100644 index 000000000..ed9e788ab --- /dev/null +++ b/app_dart/test/request_handlers/get_build_status_badge_test.dart @@ -0,0 +1,29 @@ +// Copyright 2019 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/protos.dart'; +import 'package:cocoon_service/src/request_handlers/get_build_status_badge.dart'; +import 'package:test/test.dart'; + +import '../src/datastore/fake_config.dart'; + +void main() { + final GetBuildStatusBadge handler = GetBuildStatusBadge(config: FakeConfig()); + + test('passing status', () async { + final BuildStatusResponse buildStatusResponse = BuildStatusResponse()..buildStatus = EnumBuildStatus.success; + final String response = handler.generateSVG(buildStatusResponse); + expect(response, contains('Flutter CI: passing')); + expect(response, contains(GetBuildStatusBadge.green)); + }); + + test('failing status', () async { + final BuildStatusResponse buildStatusResponse = BuildStatusResponse() + ..buildStatus = EnumBuildStatus.failure + ..failingTasks.addAll(['a', 'b', 'c']); // 3 failing tasks + final String response = handler.generateSVG(buildStatusResponse); + expect(response, contains('Flutter CI: 3 failures')); + expect(response, contains(GetBuildStatusBadge.red)); + }); +} diff --git a/app_dart/test/request_handlers/github/webhook_subscription_test.dart b/app_dart/test/request_handlers/github/webhook_subscription_test.dart index 7abf5fe24..3019a7fcb 100644 --- a/app_dart/test/request_handlers/github/webhook_subscription_test.dart +++ b/app_dart/test/request_handlers/github/webhook_subscription_test.dart @@ -1558,167 +1558,63 @@ void foo() { ); }); - test('Engine labels PRs, no comment if Java tests', () async { - const int issueNumber = 123; - - tester.message = generateGithubWebhookMessage( - action: 'opened', - number: issueNumber, - slug: Config.engineSlug, - ); - - when(pullRequestsService.listFiles(Config.engineSlug, issueNumber)).thenAnswer( - (_) => Stream.fromIterable([ - PullRequestFile()..filename = 'shell/platform/android/io/flutter/Blah.java', - PullRequestFile()..filename = 'shell/platform/android/test/io/flutter/BlahTest.java', - ]), - ); - - await tester.post(webhook); - - verifyNever( - issuesService.addLabelsToIssue(Config.engineSlug, issueNumber, any), - ); - - verifyNever( - issuesService.createComment( - Config.engineSlug, - issueNumber, - argThat(contains(config.missingTestsPullRequestMessageValue)), - ), - ); - }); - - test('Engine labels PRs, no comment if script tests', () async { - const int issueNumber = 123; - - tester.message = generateGithubWebhookMessage( - action: 'opened', - number: issueNumber, - slug: Config.engineSlug, - ); - - when(pullRequestsService.listFiles(Config.engineSlug, issueNumber)).thenAnswer( - (_) => Stream.fromIterable([ - PullRequestFile()..filename = 'fml/blah.cc', - PullRequestFile()..filename = 'fml/testing/blah_test.sh', - ]), - ); - - await tester.post(webhook); - - verifyNever( - issuesService.createComment( - Config.engineSlug, - issueNumber, - argThat(contains(config.missingTestsPullRequestMessageValue)), - ), - ); - }); - - test('Engine labels PRs, no comment if cc tests', () async { - const int issueNumber = 123; - - tester.message = generateGithubWebhookMessage( - action: 'opened', - number: issueNumber, - slug: Config.engineSlug, - ); - - when(pullRequestsService.listFiles(Config.engineSlug, issueNumber)).thenAnswer( - (_) => Stream.fromIterable([ - PullRequestFile()..filename = 'fml/blah.cc', - PullRequestFile()..filename = 'fml/blah_unittests.cc', - ]), - ); - - await tester.post(webhook); - - verifyNever( - issuesService.addLabelsToIssue( - Config.engineSlug, - issueNumber, - any, - ), - ); - - verifyNever( - issuesService.createComment( - Config.engineSlug, - issueNumber, - argThat(contains(config.missingTestsPullRequestMessageValue)), - ), - ); - }); - - test('Engine labels PRs, no comment if py tests', () async { - const int issueNumber = 123; - - tester.message = generateGithubWebhookMessage( - action: 'opened', - number: issueNumber, - slug: Config.engineSlug, - ); - - when(pullRequestsService.listFiles(Config.engineSlug, issueNumber)).thenAnswer( - (_) => Stream.fromIterable([ - PullRequestFile()..filename = 'tools/font-subset/main.cc', - PullRequestFile()..filename = 'tools/font-subset/test.py', - ]), - ); - - await tester.post(webhook); - - verifyNever( - issuesService.addLabelsToIssue( - Config.engineSlug, - issueNumber, - any, - ), - ); - - verifyNever( - issuesService.createComment( - Config.engineSlug, - issueNumber, - argThat(contains(config.missingTestsPullRequestMessageValue)), - ), - ); - }); - - test('Engine labels PRs, no comment if cc benchmarks', () async { - const int issueNumber = 123; - - tester.message = generateGithubWebhookMessage( - action: 'opened', - number: issueNumber, - slug: Config.engineSlug, - ); - - when(pullRequestsService.listFiles(Config.engineSlug, issueNumber)).thenAnswer( - (_) => Stream.fromIterable([ - PullRequestFile()..filename = 'fml/blah.cc', - PullRequestFile()..filename = 'fml/blah_benchmarks.cc', - ]), - ); + test('Engine labels PRs, no comment if tested', () async { + final List> pullRequestFileList = [ + [ + // Java tests. + 'shell/platform/android/io/flutter/Blah.java', + 'shell/platform/android/test/io/flutter/BlahTest.java', + ], + [ + // Script tests. + 'fml/blah.cc', + 'fml/testing/blah_test.sh', + ], + [ + // cc tests. + 'fml/blah.cc', + 'fml/blah_unittests.cc', + ], + [ + // cc benchmarks. + 'fml/blah.cc', + 'fml/blah_benchmarks.cc', + ], + [ + // py tests. + 'tools/font-subset/main.cc', + 'tools/font-subset/test.py', + ], + [ + // scenario app is a test. + 'scenario_app/project.pbxproj', + 'scenario_app/Info_Impeller.plist', + ], + ]; + + for (int issueNumber = 0; issueNumber < pullRequestFileList.length; issueNumber++) { + tester.message = generateGithubWebhookMessage( + action: 'opened', + number: issueNumber, + slug: Config.engineSlug, + ); - await tester.post(webhook); + when(pullRequestsService.listFiles(Config.engineSlug, issueNumber)).thenAnswer( + (_) => Stream.fromIterable( + pullRequestFileList[issueNumber].map((String filename) => PullRequestFile()..filename = filename), + ), + ); - verifyNever( - issuesService.addLabelsToIssue( - Config.engineSlug, - issueNumber, - any, - ), - ); + await tester.post(webhook); - verifyNever( - issuesService.createComment( - Config.engineSlug, - issueNumber, - argThat(contains(config.missingTestsPullRequestMessageValue)), - ), - ); + verifyNever( + issuesService.createComment( + Config.engineSlug, + issueNumber, + argThat(contains(config.missingTestsPullRequestMessageValue)), + ), + ); + } }); test('Engine labels PRs, no comments if pr is for release branches', () async { diff --git a/app_dart/test/request_handlers/scheduler/vacuum_stale_tasks_test.dart b/app_dart/test/request_handlers/scheduler/vacuum_stale_tasks_test.dart index 3135b7961..93f40fb3b 100644 --- a/app_dart/test/request_handlers/scheduler/vacuum_stale_tasks_test.dart +++ b/app_dart/test/request_handlers/scheduler/vacuum_stale_tasks_test.dart @@ -20,14 +20,6 @@ void main() { late VacuumStaleTasks handler; final Commit commit = generateCommit(1); - final DateTime now = DateTime(2023, 2, 9, 13, 37); - - /// Helper function for returning test times relative to [now]. - DateTime relativeToNow(int minutes) { - final Duration duration = Duration(minutes: minutes); - - return now.subtract(duration); - } setUp(() { config = FakeConfig(); @@ -41,32 +33,20 @@ void main() { }); test('skips when no tasks are stale', () async { - final List expectedTasks = [ + final List originalTasks = [ generateTask( 1, status: Task.statusInProgress, - created: relativeToNow(1), - parent: commit, - ), - generateTask( - 2, - status: Task.statusSucceeded, - created: relativeToNow(VacuumStaleTasks.kTimeoutLimit.inMinutes + 5), - parent: commit, - ), - generateTask( - 3, - status: Task.statusInProgress, - created: relativeToNow(VacuumStaleTasks.kTimeoutLimit.inMinutes), parent: commit, + buildNumber: 123, ), ]; - await config.db.commit(inserts: expectedTasks); + await config.db.commit(inserts: originalTasks); await tester.get(handler); final List tasks = config.db.values.values.whereType().toList(); - expect(tasks, expectedTasks); + expect(tasks[0].status, Task.statusInProgress); }); test('resets stale task', () async { @@ -74,20 +54,17 @@ void main() { generateTask( 1, status: Task.statusInProgress, - created: relativeToNow(1), parent: commit, ), generateTask( 2, status: Task.statusSucceeded, - created: relativeToNow(VacuumStaleTasks.kTimeoutLimit.inMinutes + 5), parent: commit, ), // Task 3 should be vacuumed generateTask( 3, status: Task.statusInProgress, - created: relativeToNow(VacuumStaleTasks.kTimeoutLimit.inMinutes + 1), parent: commit, ), ]; @@ -97,10 +74,10 @@ void main() { await tester.get(handler); final List tasks = config.db.values.values.whereType().toList(); - expect(tasks[0], originalTasks[0]); - expect(tasks[1], originalTasks[1]); - expect(tasks[2].status, Task.statusNew); + expect(tasks[0].createTimestamp, 0); + expect(tasks[0].status, Task.statusNew); expect(tasks[2].createTimestamp, 0); + expect(tasks[2].status, Task.statusNew); }); }); } diff --git a/app_dart/test/server_test.dart b/app_dart/test/server_test.dart new file mode 100644 index 000000000..addb63d98 --- /dev/null +++ b/app_dart/test/server_test.dart @@ -0,0 +1,35 @@ +// Copyright 2019 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/cocoon_service.dart'; +import 'package:cocoon_service/server.dart'; +import 'package:cocoon_service/src/service/commit_service.dart'; +import 'package:test/test.dart'; + +import 'src/datastore/fake_config.dart'; +import 'src/request_handling/fake_authentication.dart'; +import 'src/service/fake_buildbucket.dart'; +import 'src/service/fake_gerrit_service.dart'; +import 'src/service/fake_luci_build_service.dart'; +import 'src/service/fake_scheduler.dart'; + +void main() { + test('verify server can be created', () { + createServer( + config: FakeConfig( + webhookKeyValue: 'fake-secret', + ), + cache: CacheService(inMemory: true), + authProvider: FakeAuthenticationProvider(), + swarmingAuthProvider: FakeAuthenticationProvider(), + branchService: BranchService(config: FakeConfig(), gerritService: FakeGerritService()), + buildBucketClient: FakeBuildBucketClient(), + luciBuildService: FakeLuciBuildService(config: FakeConfig()), + githubChecksService: GithubChecksService(FakeConfig()), + commitService: CommitService(config: FakeConfig()), + gerritService: FakeGerritService(), + scheduler: FakeScheduler(config: FakeConfig()), + ); + }); +} diff --git a/app_dart/test/src/datastore/fake_datastore.dart b/app_dart/test/src/datastore/fake_datastore.dart index f749dd42b..93540f0ec 100644 --- a/app_dart/test/src/datastore/fake_datastore.dart +++ b/app_dart/test/src/datastore/fake_datastore.dart @@ -136,8 +136,9 @@ class FakeDatastoreDB implements DatastoreDB { } @override - Future lookupOrNull>(Key key) { - throw UnimplementedError(); + Future lookupOrNull>(Key key) async { + final values = await lookup(>[key]); + return values.firstOrNull as T?; } } diff --git a/auto_submit/Dockerfile b/auto_submit/Dockerfile index d8a40648c..d83a76cb1 100644 --- a/auto_submit/Dockerfile +++ b/auto_submit/Dockerfile @@ -4,7 +4,7 @@ # Dart Docker official images can be found here: https://hub.docker.com/_/dart -FROM dart:beta@sha256:e05aaad0f71e9d045e2a45b63a65f85e5e466b1cc1de34e7e684eede45a29d9b +FROM dart:beta@sha256:4bddd6ccaed32d2d2b8c27bcd71b1ebca13cec47f3abe7b12f640f9432da8e83 WORKDIR /app diff --git a/auto_submit/lib/validations/ci_successful.dart b/auto_submit/lib/validations/ci_successful.dart index 86197db14..27f8c8774 100644 --- a/auto_submit/lib/validations/ci_successful.dart +++ b/auto_submit/lib/validations/ci_successful.dart @@ -146,7 +146,7 @@ class CiSuccessful extends Validation { final String? name = status.context; // If the account author is a roller account do not block merge on flutter-gold check. - if (isEngineRoller(author, slug) && name == 'flutter-gold') { + if (isToEngineRoller(author, slug) && name == 'flutter-gold') { log.info('Skipping status check for flutter-gold for ${slug.fullName}/$prNumber, pr author: $author.'); continue; } @@ -159,7 +159,7 @@ class CiSuccessful extends Validation { if (status.state == STATUS_FAILURE && !notInAuthorsControl.contains(name)) { failures.add(FailureDetail(name!, status.targetUrl!)); } - if (status.state == STATUS_PENDING && isStale(status.createdAt!) && isEngineRoller(author, slug)) { + if (status.state == STATUS_PENDING && isStale(status.createdAt!) && supportStale(author, slug)) { staleStatuses.add(status); } } @@ -191,10 +191,6 @@ class CiSuccessful extends Validation { for (github.CheckRun checkRun in checkRuns) { final String? name = checkRun.name; - if (isStale(checkRun.startedAt) && isEngineRoller(author, slug)) { - staleCheckRuns.add(checkRun); - } - if (checkRun.conclusion == github.CheckRunConclusion.skipped || checkRun.conclusion == github.CheckRunConclusion.success || (checkRun.status == github.CheckRunStatus.completed && @@ -205,6 +201,10 @@ class CiSuccessful extends Validation { // checkrun has failed. log.info('${slug.name}/$prNumber: CheckRun $name failed.'); failures.add(FailureDetail(name!, checkRun.detailsUrl as String)); + } else if (checkRun.status == github.CheckRunStatus.queued) { + if (isStale(checkRun.startedAt) && supportStale(author, slug)) { + staleCheckRuns.add(checkRun); + } } allSuccess = false; } @@ -222,7 +222,19 @@ class CiSuccessful extends Validation { return dateTime.compareTo(DateTime.now().subtract(const Duration(hours: Config.kGitHubCheckStaleThreshold))) < 0; } - bool isEngineRoller(Author author, github.RepositorySlug slug) { + /// Perform stale check only on Engine related rolled PRs. + /// + /// This includes those rolled PRs from upstream to Engine repo and those + /// rolled PRs from Engine to Framework. + bool supportStale(Author author, github.RepositorySlug slug) { + return isToEngineRoller(author, slug) || isEngineToFrameworkRoller(author, slug); + } + + bool isToEngineRoller(Author author, github.RepositorySlug slug) { return config.rollerAccounts.contains(author.login!) && slug == Config.engineSlug; } + + bool isEngineToFrameworkRoller(Author author, github.RepositorySlug slug) { + return author.login! == 'engine-flutter-autoroll' && slug == Config.flutterSlug; + } } diff --git a/auto_submit/pubspec.yaml b/auto_submit/pubspec.yaml index feed5178a..921bafd33 100644 --- a/auto_submit/pubspec.yaml +++ b/auto_submit/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: github: 9.20.0 googleapis: 11.4.0 googleapis_auth: 1.4.1 - graphql: 5.2.0-beta.6 + graphql: 5.2.0-beta.7 gql: 1.0.1-alpha+1691943394579 http: 1.1.2 json_annotation: 4.8.1 @@ -35,7 +35,7 @@ dev_dependencies: json_serializable: 6.7.1 flutter_lints: 3.0.1 mockito: 5.4.4 - test: 1.24.9 + test: 1.25.0 builders: json_serializable: 3.3.0 diff --git a/auto_submit/test/validations/ci_successful_test.dart b/auto_submit/test/validations/ci_successful_test.dart index 1d6cc9ed8..e325118b2 100644 --- a/auto_submit/test/validations/ci_successful_test.dart +++ b/auto_submit/test/validations/ci_successful_test.dart @@ -618,7 +618,7 @@ void main() { }); test('when it is engine roller', () async { - final bool isEngineRoller = ciSuccessful.isEngineRoller( + final bool isEngineRoller = ciSuccessful.isToEngineRoller( Author(login: 'engine-flutter-autoroll'), github.RepositorySlug('flutter', 'engine'), ); @@ -626,15 +626,45 @@ void main() { }); test('when it is not from roller', () async { final bool isEngineRoller = - ciSuccessful.isEngineRoller(Author(login: 'testAuthor'), github.RepositorySlug('flutter', 'engine')); + ciSuccessful.isToEngineRoller(Author(login: 'testAuthor'), github.RepositorySlug('flutter', 'engine')); expect(isEngineRoller, false); }); test('when it is not from engine', () async { - final bool isEngineRoller = ciSuccessful.isEngineRoller( + final bool isEngineRoller = ciSuccessful.isToEngineRoller( Author(login: 'engine-flutter-autoroll'), github.RepositorySlug('flutter', 'flutter'), ); expect(isEngineRoller, false); }); }); + + group('Validate if an engine to framework roller', () { + setUp(() { + githubService = FakeGithubService(client: MockGitHub()); + config = FakeConfig(githubService: githubService); + ciSuccessful = CiSuccessful(config: config); + }); + + test('when it is engine roller', () async { + final bool isEngineRoller = ciSuccessful.isEngineToFrameworkRoller( + Author(login: 'engine-flutter-autoroll'), + github.RepositorySlug('flutter', 'flutter'), + ); + expect(isEngineRoller, true); + }); + test('when it is not from roller', () async { + final bool isEngineRoller = ciSuccessful.isEngineToFrameworkRoller( + Author(login: 'testAuthor'), + github.RepositorySlug('flutter', 'flutter'), + ); + expect(isEngineRoller, false); + }); + test('when it is not from framework', () async { + final bool isEngineRoller = ciSuccessful.isEngineToFrameworkRoller( + Author(login: 'engine-flutter-autoroll'), + github.RepositorySlug('flutter', 'engine'), + ); + expect(isEngineRoller, false); + }); + }); } diff --git a/cipd_packages/device_doctor/pubspec.yaml b/cipd_packages/device_doctor/pubspec.yaml index a854f8533..6115f017d 100644 --- a/cipd_packages/device_doctor/pubspec.yaml +++ b/cipd_packages/device_doctor/pubspec.yaml @@ -19,4 +19,4 @@ dev_dependencies: build_runner: 2.4.7 fake_async: 1.3.1 mockito: 5.4.4 - test: 1.24.9 + test: 1.25.0 diff --git a/cloud_build/get_docker_image_provenance.sh b/cloud_build/get_docker_image_provenance.sh index 2d40040ab..e36afae6f 100755 --- a/cloud_build/get_docker_image_provenance.sh +++ b/cloud_build/get_docker_image_provenance.sh @@ -6,7 +6,9 @@ # This script is used to pull a docker image's provenance and save it to a file. DOCKER_IMAGE_URL=$1 OUTPUT_DIRECTORY=$2 -# Getting the docker image provenance can be flaky, so retry up to 3 times. +# Getting the docker image provenance can be flaky due to the provenance not +# uploading fast enough, or a transient error from artifact registry, so retry +# up to 3 times. MAX_ATTEMPTS=3 # Download the jq binary in order to obtain the artifact registry url from the @@ -18,10 +20,8 @@ for attempt in $(seq 1 $MAX_ATTEMPTS) do echo "(Attempt $attempt) Obtaining provenance for $1" gcloud artifacts docker images describe \ - $DOCKER_IMAGE_URL --show-provenance --format json > tmp.json + $DOCKER_IMAGE_URL --show-provenance --format json > $OUTPUT_DIRECTORY COMMAND_RESULT=$? - val=$(cat tmp.json | jq -r '.provenance_summary.provenance[0].envelope.payload' | base64 -d | jq '.predicate.recipe.arguments.sourceProvenance') - cat tmp.json | jq ".provenance_summary.provenance[0].build.intotoStatement.slsaProvenance.recipe.arguments.sourceProvenance = ${val}" > $OUTPUT_DIRECTORY if [[ $COMMAND_RESULT -eq 0 ]] then echo "Successfully obtained provenance and saved to $2" diff --git a/cron.yaml b/cron.yaml index 43e261814..bf1525e53 100644 --- a/cron.yaml +++ b/cron.yaml @@ -7,12 +7,11 @@ cron: url: /api/vacuum-github-commits schedule: every 6 hours -# Disabled for https://github.com/flutter/flutter/issues/121989 # TODO(keyonghan): will delete if `In Progress` hanging issue is resolved: # https://github.com/flutter/flutter/issues/120395#issuecomment-1444810718 -#- description: vacuum stale tasks -# url: /api/scheduler/vacuum-stale-tasks -# schedule: every 10 minutes +- description: vacuum stale tasks + url: /api/scheduler/vacuum-stale-tasks + schedule: every 12 hours - description: backfills builds url: /api/scheduler/batch-backfiller diff --git a/gh_actions/third_party/no-response/package-lock.json b/gh_actions/third_party/no-response/package-lock.json index df4e47d30..254293c5d 100644 --- a/gh_actions/third_party/no-response/package-lock.json +++ b/gh_actions/third_party/no-response/package-lock.json @@ -11,18 +11,18 @@ "dependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.0", - "eslint-plugin-prettier": "^5.0.1", + "eslint-plugin-prettier": "^5.1.2", "scramjet": "^4.37.0" }, "devDependencies": { "@octokit/webhooks-types": "^7.3.1", "@types/jest": "^29.5.11", - "@types/node": "^20.10.4", - "@typescript-eslint/parser": "^6.14.0", + "@types/node": "^20.10.5", + "@typescript-eslint/parser": "^6.17.0", "@vercel/ncc": "^0.38.1", - "eslint": "^8.55.0", + "eslint": "^8.56.0", "eslint-plugin-github": "^4.10.1", - "eslint-plugin-jest": "^27.6.0", + "eslint-plugin-jest": "^27.6.1", "extract-pr-titles": "^1.1.0", "jest": "^29.7.0", "js-yaml": "^4.1.0", @@ -771,9 +771,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", - "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -1554,9 +1554,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.10.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", - "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==", + "version": "20.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", + "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -1640,15 +1640,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.14.0.tgz", - "integrity": "sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.17.0.tgz", + "integrity": "sha512-C4bBaX2orvhK+LlwrY8oWGmSl4WolCfYm513gEccdWZj0CwGadbIADb0FtVEcI+WzUyjyoBj2JRP8g25E6IB8A==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.14.0", - "@typescript-eslint/types": "6.14.0", - "@typescript-eslint/typescript-estree": "6.14.0", - "@typescript-eslint/visitor-keys": "6.14.0", + "@typescript-eslint/scope-manager": "6.17.0", + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/typescript-estree": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0", "debug": "^4.3.4" }, "engines": { @@ -1668,13 +1668,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.14.0.tgz", - "integrity": "sha512-VT7CFWHbZipPncAZtuALr9y3EuzY1b1t1AEkIq2bTXUPKw+pHoXflGNG5L+Gv6nKul1cz1VH8fz16IThIU0tdg==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.17.0.tgz", + "integrity": "sha512-RX7a8lwgOi7am0k17NUO0+ZmMOX4PpjLtLRgLmT1d3lBYdWH4ssBUbwdmc5pdRX8rXon8v9x8vaoOSpkHfcXGA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.14.0", - "@typescript-eslint/visitor-keys": "6.14.0" + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1685,9 +1685,9 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.14.0.tgz", - "integrity": "sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.17.0.tgz", + "integrity": "sha512-qRKs9tvc3a4RBcL/9PXtKSehI/q8wuU9xYJxe97WFxnzH8NWWtcW3ffNS+EWg8uPvIerhjsEZ+rHtDqOCiH57A==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1698,16 +1698,17 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.14.0.tgz", - "integrity": "sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.17.0.tgz", + "integrity": "sha512-gVQe+SLdNPfjlJn5VNGhlOhrXz4cajwFd5kAgWtZ9dCZf4XJf8xmgCTLIqec7aha3JwgLI2CK6GY1043FRxZwg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.14.0", - "@typescript-eslint/visitor-keys": "6.14.0", + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", + "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, @@ -1725,12 +1726,12 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.14.0.tgz", - "integrity": "sha512-fB5cw6GRhJUz03MrROVuj5Zm/Q+XWlVdIsFj+Zb1Hvqouc8t+XP2H5y53QYU/MGtd2dPg6/vJJlhoX3xc2ehfw==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.17.0.tgz", + "integrity": "sha512-H6VwB/k3IuIeQOyYczyyKN8wH6ed8EwliaYHLxOIhyF0dYEIsN8+Bk3GE19qafeMKyZJJHP8+O1HiFhFLUNKSg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.14.0", + "@typescript-eslint/types": "6.17.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1741,6 +1742,30 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/parser/node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -2316,9 +2341,9 @@ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" }, "node_modules/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", "engines": { "node": ">=0.6" } @@ -2969,14 +2994,14 @@ } }, "node_modules/eslint": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", - "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.55.0", + "@eslint/js": "8.56.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -3026,7 +3051,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", - "dev": true, + "devOptional": true, "bin": { "eslint-config-prettier": "bin/cli.js" } @@ -3210,9 +3235,9 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "27.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.0.tgz", - "integrity": "sha512-MTlusnnDMChbElsszJvrwD1dN3x6nZl//s4JD23BxB6MgR66TZlL064su24xEIS3VACfAoHV1vgyMgPw8nkdng==", + "version": "27.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.1.tgz", + "integrity": "sha512-WEYkyVXD9NlmFBKvrkmzrC+C9yZoz5pAml2hO19PlS3spJtoiwj4p2u8spd/7zx5IvRsZsCmsoImaAvBB9X93Q==", "dev": true, "dependencies": { "@typescript-eslint/utils": "^5.10.0" @@ -3411,22 +3436,23 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", - "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.2.tgz", + "integrity": "sha512-dhlpWc9vOwohcWmClFcA+HjlvUpuyynYs0Rf+L/P6/0iQE6vlHW9l5bkfzN62/Stm9fbq8ku46qzde76T1xlSg==", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.5" + "synckit": "^0.8.6" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/prettier" + "url": "https://opencollective.com/eslint-plugin-prettier" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", + "eslint-config-prettier": "*", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -3665,9 +3691,9 @@ } }, "node_modules/execa/node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", + "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", "dependencies": { "path-key": "^4.0.0" }, @@ -6717,12 +6743,12 @@ "dev": true }, "node_modules/synckit": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", - "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.6.tgz", + "integrity": "sha512-laHF2savN6sMeHCjLRkheIU4wo3Zg9Ln5YOjOo7sZ5dVQW8yF5pPE5SIw1dsPhq3TRp1jisKRCdPhfs/1WMqDA==", "dependencies": { - "@pkgr/utils": "^2.3.1", - "tslib": "^2.5.0" + "@pkgr/utils": "^2.4.2", + "tslib": "^2.6.2" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -7874,9 +7900,9 @@ } }, "@eslint/js": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", - "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==" + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==" }, "@fastify/busboy": { "version": "2.0.0", @@ -8504,9 +8530,9 @@ "dev": true }, "@types/node": { - "version": "20.10.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", - "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==", + "version": "20.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", + "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", "dev": true, "requires": { "undici-types": "~5.26.4" @@ -8570,59 +8596,78 @@ } }, "@typescript-eslint/parser": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.14.0.tgz", - "integrity": "sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.17.0.tgz", + "integrity": "sha512-C4bBaX2orvhK+LlwrY8oWGmSl4WolCfYm513gEccdWZj0CwGadbIADb0FtVEcI+WzUyjyoBj2JRP8g25E6IB8A==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "6.14.0", - "@typescript-eslint/types": "6.14.0", - "@typescript-eslint/typescript-estree": "6.14.0", - "@typescript-eslint/visitor-keys": "6.14.0", + "@typescript-eslint/scope-manager": "6.17.0", + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/typescript-estree": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0", "debug": "^4.3.4" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.14.0.tgz", - "integrity": "sha512-VT7CFWHbZipPncAZtuALr9y3EuzY1b1t1AEkIq2bTXUPKw+pHoXflGNG5L+Gv6nKul1cz1VH8fz16IThIU0tdg==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.17.0.tgz", + "integrity": "sha512-RX7a8lwgOi7am0k17NUO0+ZmMOX4PpjLtLRgLmT1d3lBYdWH4ssBUbwdmc5pdRX8rXon8v9x8vaoOSpkHfcXGA==", "dev": true, "requires": { - "@typescript-eslint/types": "6.14.0", - "@typescript-eslint/visitor-keys": "6.14.0" + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0" } }, "@typescript-eslint/types": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.14.0.tgz", - "integrity": "sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.17.0.tgz", + "integrity": "sha512-qRKs9tvc3a4RBcL/9PXtKSehI/q8wuU9xYJxe97WFxnzH8NWWtcW3ffNS+EWg8uPvIerhjsEZ+rHtDqOCiH57A==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.14.0.tgz", - "integrity": "sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.17.0.tgz", + "integrity": "sha512-gVQe+SLdNPfjlJn5VNGhlOhrXz4cajwFd5kAgWtZ9dCZf4XJf8xmgCTLIqec7aha3JwgLI2CK6GY1043FRxZwg==", "dev": true, "requires": { - "@typescript-eslint/types": "6.14.0", - "@typescript-eslint/visitor-keys": "6.14.0", + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", + "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/visitor-keys": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.14.0.tgz", - "integrity": "sha512-fB5cw6GRhJUz03MrROVuj5Zm/Q+XWlVdIsFj+Zb1Hvqouc8t+XP2H5y53QYU/MGtd2dPg6/vJJlhoX3xc2ehfw==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.17.0.tgz", + "integrity": "sha512-H6VwB/k3IuIeQOyYczyyKN8wH6ed8EwliaYHLxOIhyF0dYEIsN8+Bk3GE19qafeMKyZJJHP8+O1HiFhFLUNKSg==", "dev": true, "requires": { - "@typescript-eslint/types": "6.14.0", + "@typescript-eslint/types": "6.17.0", "eslint-visitor-keys": "^3.4.1" } }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, "semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -9024,9 +9069,9 @@ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" }, "big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==" + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==" }, "bplist-parser": { "version": "0.2.0", @@ -9489,14 +9534,14 @@ "dev": true }, "eslint": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", - "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.55.0", + "@eslint/js": "8.56.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -9587,7 +9632,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", - "dev": true + "devOptional": true }, "eslint-import-resolver-node": { "version": "0.3.9", @@ -9740,9 +9785,9 @@ } }, "eslint-plugin-jest": { - "version": "27.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.0.tgz", - "integrity": "sha512-MTlusnnDMChbElsszJvrwD1dN3x6nZl//s4JD23BxB6MgR66TZlL064su24xEIS3VACfAoHV1vgyMgPw8nkdng==", + "version": "27.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.1.tgz", + "integrity": "sha512-WEYkyVXD9NlmFBKvrkmzrC+C9yZoz5pAml2hO19PlS3spJtoiwj4p2u8spd/7zx5IvRsZsCmsoImaAvBB9X93Q==", "dev": true, "requires": { "@typescript-eslint/utils": "^5.10.0" @@ -9863,12 +9908,12 @@ "dev": true }, "eslint-plugin-prettier": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", - "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.2.tgz", + "integrity": "sha512-dhlpWc9vOwohcWmClFcA+HjlvUpuyynYs0Rf+L/P6/0iQE6vlHW9l5bkfzN62/Stm9fbq8ku46qzde76T1xlSg==", "requires": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.5" + "synckit": "^0.8.6" } }, "eslint-rule-documentation": { @@ -9965,9 +10010,9 @@ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==" }, "npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", + "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", "requires": { "path-key": "^4.0.0" } @@ -12158,12 +12203,12 @@ "dev": true }, "synckit": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", - "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.6.tgz", + "integrity": "sha512-laHF2savN6sMeHCjLRkheIU4wo3Zg9Ln5YOjOo7sZ5dVQW8yF5pPE5SIw1dsPhq3TRp1jisKRCdPhfs/1WMqDA==", "requires": { - "@pkgr/utils": "^2.3.1", - "tslib": "^2.5.0" + "@pkgr/utils": "^2.4.2", + "tslib": "^2.6.2" } }, "test-exclude": { diff --git a/gh_actions/third_party/no-response/package.json b/gh_actions/third_party/no-response/package.json index c6de9e930..2c42bbca3 100644 --- a/gh_actions/third_party/no-response/package.json +++ b/gh_actions/third_party/no-response/package.json @@ -27,18 +27,18 @@ "dependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.0", - "eslint-plugin-prettier": "^5.0.1", + "eslint-plugin-prettier": "^5.1.2", "scramjet": "^4.37.0" }, "devDependencies": { "@octokit/webhooks-types": "^7.3.1", "@types/jest": "^29.5.11", - "@types/node": "^20.10.4", - "@typescript-eslint/parser": "^6.14.0", + "@types/node": "^20.10.5", + "@typescript-eslint/parser": "^6.17.0", "@vercel/ncc": "^0.38.1", - "eslint": "^8.55.0", + "eslint": "^8.56.0", "eslint-plugin-github": "^4.10.1", - "eslint-plugin-jest": "^27.6.0", + "eslint-plugin-jest": "^27.6.1", "extract-pr-titles": "^1.1.0", "jest": "^29.7.0", "js-yaml": "^4.1.0", diff --git a/licenses/pubspec.yaml b/licenses/pubspec.yaml index d6b58fec4..40c3847f0 100644 --- a/licenses/pubspec.yaml +++ b/licenses/pubspec.yaml @@ -10,4 +10,4 @@ dependencies: dev_dependencies: mockito: 5.4.4 - test_api: 0.6.1 + test_api: 0.7.0