diff --git a/.github/workflows/deploy_artifacts_proxy.yaml b/.github/workflows/deploy_artifacts_proxy.yaml new file mode 100644 index 00000000..32b3350f --- /dev/null +++ b/.github/workflows/deploy_artifacts_proxy.yaml @@ -0,0 +1,62 @@ +name: Deploy Artifacts Proxy + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - .github/workflows/deploy_artifacts_proxy.yaml + - packages/artifacts_proxy/**" + +env: + PROJECT_ID: code-push-prod + SERVICE: artifacts-proxy + REGION: us-central1 + +jobs: + deploy: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: packages/artifacts_proxy + + name: ☁️ Artifacts Proxy + + steps: + - name: 📚 Git Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + + - uses: dart-lang/setup-dart@v1 + + - name: Setup Cloud SDK + uses: google-github-actions/setup-gcloud@v0.2.0 + with: + project_id: ${{ env.PROJECT_ID }} + service_account_key: ${{ secrets.CLOUD_RUN_SA_PROD }} + export_default_credentials: true + + - name: Authorize Docker Push + run: gcloud auth configure-docker + + - name: Build and Push Container + run: |- + docker build -t gcr.io/${{ env.PROJECT_ID }}/${{ env.SERVICE }}:${{ github.sha }} . + docker push gcr.io/${{ env.PROJECT_ID }}/${{ env.SERVICE }}:${{ github.sha }} + + - name: Deploy to Cloud Run + id: deploy + uses: google-github-actions/deploy-cloudrun@v0.4.0 + with: + service: ${{ env.SERVICE }} + image: gcr.io/${{ env.PROJECT_ID }}/${{ env.SERVICE }}:${{ github.sha }} + region: ${{ env.REGION }} + + - name: Show Output + run: echo ${{ steps.deploy.outputs.url }} + + - name: Ping + run: curl "${{ steps.deploy.outputs.url }}" diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 3253ad96..542496a5 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -32,10 +32,13 @@ jobs: id: needs_dart_build with: filters: | + artifacts_proxy: + - ./.github/actions/dart_package + - packages/artifacts_proxy/** shorebird_cli: - ./.github/actions/dart_package - packages/shorebird_cli/** - - packages/shorebird_code_push_client/** + - packages/shorebird_code_push_client/** shorebird_code_push_client: - ./.github/actions/dart_package - packages/shorebird_code_push_client/** diff --git a/packages/artifact_proxy/.dockerignore b/packages/artifact_proxy/.dockerignore new file mode 100644 index 00000000..34f7a00f --- /dev/null +++ b/packages/artifact_proxy/.dockerignore @@ -0,0 +1,10 @@ +.dockerignore +Dockerfile +build/ +.dart_tool/ +.git/ +.github/ +.gitignore +.idea/ +.packages +coverage/ \ No newline at end of file diff --git a/packages/artifact_proxy/.gitignore b/packages/artifact_proxy/.gitignore new file mode 100644 index 00000000..2d29cdcd --- /dev/null +++ b/packages/artifact_proxy/.gitignore @@ -0,0 +1,6 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Test related files +coverage/ \ No newline at end of file diff --git a/packages/artifact_proxy/Dockerfile b/packages/artifact_proxy/Dockerfile new file mode 100644 index 00000000..84f018c5 --- /dev/null +++ b/packages/artifact_proxy/Dockerfile @@ -0,0 +1,23 @@ +# Official Dart image: https://hub.docker.com/_/dart +# Specify the Dart SDK base image version using dart: (ex: dart:2.12) +FROM dart:stable AS build + +# Resolve app dependencies. +WORKDIR /app + +# Copy app source code and AOT compile it. +COPY . . + +# Ensure packages are still up-to-date if anything has changed +RUN dart pub get --offline +RUN dart compile exe bin/server.dart -o bin/server + +# Build minimal serving image from AOT-compiled `/server` and required system +# libraries and configuration files stored in `/runtime/` from the build stage. +FROM scratch +COPY --from=build /runtime/ / +COPY --from=build /app/bin/server /app/bin/ + +# Start server. +EXPOSE 8080 +CMD ["/app/bin/server"] diff --git a/packages/artifact_proxy/README.md b/packages/artifact_proxy/README.md new file mode 100644 index 00000000..19877443 --- /dev/null +++ b/packages/artifact_proxy/README.md @@ -0,0 +1,26 @@ +# Artifact Proxy + +This is a tool for proxying Flutter artifacts from a derived Flutter engine +revision back to the base Flutter engine revision. This is useful for +when you need to modify _some_ of the Flutter artifacts but not all of them. + +This is a development tool which map requests to Google +Storage (either Shorebird's bucket or the official Flutter buckets). + +## Usage + +Uses `config.yaml` to configure the engine revisions and artifact overrides. + +```bash +# Run locally with hot-reload enabled. +DEV=true dart --enable-vm-service run bin/server.dart +``` + +And then in a separate terminal: + +``` +FLUTTER_STORAGE_BASE_URL=http://localhost:8080 flutter precache -a +``` + +You should use a separate checkout of Flutter when running this, so you don't +poison the cache of your main Flutter checkout. diff --git a/packages/artifact_proxy/analysis_options.yaml b/packages/artifact_proxy/analysis_options.yaml new file mode 100644 index 00000000..84e34fba --- /dev/null +++ b/packages/artifact_proxy/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.4.0.0.yaml diff --git a/packages/artifact_proxy/bin/server.dart b/packages/artifact_proxy/bin/server.dart new file mode 100644 index 00000000..7f222dca --- /dev/null +++ b/packages/artifact_proxy/bin/server.dart @@ -0,0 +1,32 @@ +// ignore_for_file: avoid_print + +import 'dart:io'; + +import 'package:artifact_proxy/artifact_proxy.dart'; +import 'package:shelf/shelf.dart'; +import 'package:shelf/shelf_io.dart' as shelf_io; +import 'package:shelf_hotreload/shelf_hotreload.dart'; +import 'package:yaml/yaml.dart'; + +Future main() async { + const configPath = 'config.yaml'; + final isDev = Platform.environment['DEV'] == 'true'; + final config = loadYaml(File(configPath).readAsStringSync()) as Map; + final handler = artifactProxyHandler(config: config); + final ip = InternetAddress.anyIPv6; + final port = int.parse(Platform.environment['PORT'] ?? '8080'); + + // Hot-reload is enabled when the DEBUG environment variable is set to true. + if (isDev) return withHotreload(() => serve(handler, ip, port)); + + await serve(handler, ip, port); +} + +Future serve(Handler proxy, InternetAddress ip, int port) async { + const pipeline = Pipeline(); + final handler = pipeline.addMiddleware(logRequests()).addHandler(proxy); + final server = await shelf_io.serve(handler, ip, port); + print('Serving at http://localhost:${server.port}'); + server.autoCompress = true; + return server; +} diff --git a/packages/artifact_proxy/config.yaml b/packages/artifact_proxy/config.yaml new file mode 100644 index 00000000..6dd4d6a4 --- /dev/null +++ b/packages/artifact_proxy/config.yaml @@ -0,0 +1,25 @@ +engine_mappings: + 79f4c5321a581f580a9bda01ec372cbf4a53aa53: + flutter_engine_revision: 9aa7816315095c86410527932918c718cb35e7d6 + shorebird_storage_bucket: download.shorebird.dev + shorebird_artifact_overrides: + # artifacts.zip + - flutter_infra_release/flutter/$engine/android-arm-64-release/artifacts.zip + - flutter_infra_release/flutter/$engine/android-arm-release/artifacts.zip + - flutter_infra_release/flutter/$engine/android-x64-release/artifacts.zip + + # embedding release + - download.flutter.io/io/flutter/flutter_embedding_release/1.0.0-$engine/flutter_embedding_release-1.0.0-$engine.pom + - download.flutter.io/io/flutter/flutter_embedding_release/1.0.0-$engine/flutter_embedding_release-1.0.0-$engine.jar + + # arm64_v8a release + - download.flutter.io/io/flutter/arm64_v8a_release/1.0.0-$engine/arm64_v8a_release-1.0.0-$engine.pom + - download.flutter.io/io/flutter/arm64_v8a_release/1.0.0-$engine/arm64_v8a_release-1.0.0-$engine.jar + + # armeabi_v7a release + - download.flutter.io/io/flutter/armeabi_v7a_release/1.0.0-$engine/armeabi_v7a_release-1.0.0-$engine.pom + - download.flutter.io/io/flutter/armeabi_v7a_release/1.0.0-$engine/armeabi_v7a_release-1.0.0-$engine.jar + + # x86_64 release + - download.flutter.io/io/flutter/x86_64_release/1.0.0-$engine/x86_64_release-1.0.0-$engine.pom + - download.flutter.io/io/flutter/x86_64_release/1.0.0-$engine/x86_64_release-1.0.0-$engine.jar diff --git a/packages/artifact_proxy/lib/artifact_proxy.dart b/packages/artifact_proxy/lib/artifact_proxy.dart new file mode 100644 index 00000000..bbf0e11d --- /dev/null +++ b/packages/artifact_proxy/lib/artifact_proxy.dart @@ -0,0 +1,88 @@ +// ignore_for_file: avoid_print + +import 'package:collection/collection.dart'; +import 'package:shelf/shelf.dart'; + +/// A [Handler] that proxies artifact requests to the correct location. +/// This is determined based on the [config]. +Handler artifactProxyHandler({required Map config}) { + final engineMappings = config['engine_mappings'] as Map; + final shorebirdEngineRevisions = engineMappings.keys.cast(); + + return (Request request) { + final path = request.url.path; + final shorebirdEngineRevision = shorebirdEngineRevisions.firstWhereOrNull( + path.contains, + ); + + final normalizedPath = shorebirdEngineRevision != null + ? path.replaceAll(shorebirdEngineRevision, r'$engine') + : path; + + if (shorebirdEngineRevision == null) { + final location = getFlutterArtifactLocation(artifactPath: normalizedPath); + print('No engine revision detected, forwarding to: $location'); + return Response.found(location); + } + + final engineMapping = engineMappings[shorebirdEngineRevision] as Map; + final shorebirdOverrides = + engineMapping['shorebird_artifact_overrides'] as List; + final flutterEngineRevision = + engineMapping['flutter_engine_revision'] as String; + final shorebirdStorageBucket = + engineMapping['shorebird_storage_bucket'] as String; + final shouldOverride = shorebirdOverrides.contains(normalizedPath); + + if (shouldOverride) { + final location = getShorebirdArtifactLocation( + artifactPath: normalizedPath, + engine: shorebirdEngineRevision, + bucket: shorebirdStorageBucket, + ); + print('Shorebird artifact detected, forwarding to: $location'); + return Response.found(location); + } + + final location = getFlutterArtifactLocation( + artifactPath: normalizedPath, + engine: flutterEngineRevision, + ); + print('Flutter artifact detected, forwarding to: $location'); + return Response.found(location); + }; +} + +/// Returns the location of the artifact at [artifactPath] using the +/// specified [engine] revision for original Flutter artifacts. +String getFlutterArtifactLocation({ + required String artifactPath, + String? engine, +}) { + final adjustedPath = engine != null + ? artifactPath.replaceAll(r'$engine', engine) + : artifactPath; + + final isChromeInfra = adjustedPath.contains('flutter_infra_release/cipd'); + + /// TODO(felangel): remove this after 3.8 is released. + if (isChromeInfra) { + return adjustedPath.replaceAll( + 'flutter_infra_release/cipd', + 'https://chrome-infra-packages.appspot.com/dl', + ); + } + + return 'https://storage.googleapis.com/$adjustedPath'; +} + +/// Returns the location of the artifact at [artifactPath] using the +/// specified [engine] revision for Shorebird artifacts. +String getShorebirdArtifactLocation({ + required String artifactPath, + required String engine, + required String bucket, +}) { + final adjustedPath = artifactPath.replaceAll(r'$engine', engine); + return 'https://storage.googleapis.com/$bucket/$adjustedPath'; +} diff --git a/packages/artifact_proxy/pubspec.lock b/packages/artifact_proxy/pubspec.lock new file mode 100644 index 00000000..1b3f4ebc --- /dev/null +++ b/packages/artifact_proxy/pubspec.lock @@ -0,0 +1,405 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: a36ec4843dc30ea6bf652bf25e3448db6c5e8bcf4aa55f063a5d1dad216d8214 + url: "https://pub.dev" + source: hosted + version: "58.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: cc4242565347e98424ce9945c819c192ec0838cb9d1f6aa4a97cc96becbc5b27 + url: "https://pub.dev" + source: hosted + version: "5.10.0" + args: + dependency: transitive + description: + name: args + sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + collection: + dependency: "direct main" + description: + name: collection + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" + source: hosted + version: "1.17.1" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" + url: "https://pub.dev" + source: hosted + version: "1.6.3" + crypto: + dependency: transitive + description: + name: crypto + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" + source: hosted + version: "3.0.2" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + glob: + dependency: transitive + description: + name: glob + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: "728c0613556c1d153f7e7f4a367cffacc3f5a677d7f6497a1c2b35add4e6dacf" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + logging: + dependency: transitive + description: + name: logging + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + url: "https://pub.dev" + source: hosted + version: "0.12.15" + meta: + dependency: transitive + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "80a996cd9a69284b3dc521ce185ffe9150cde69767c2d3a0720147d93c0cef53" + url: "https://pub.dev" + source: hosted + version: "0.3.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + shelf: + dependency: "direct main" + description: + name: shelf + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + url: "https://pub.dev" + source: hosted + version: "1.4.0" + shelf_hotreload: + dependency: "direct main" + description: + name: shelf_hotreload + sha256: "5e7a68273d64a095384325d769c6a4885ea15c6aafee361098c0d8a5ecb5dcf6" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: aef74dc9195746a384843102142ab65b6a4735bb3beea791e63527b88cc83306 + url: "https://pub.dev" + source: hosted + version: "3.0.1" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: e792b76b96a36d4a41b819da593aff4bdd413576b3ba6150df5d8d9996d2e74c + url: "https://pub.dev" + source: hosted + version: "1.1.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: transitive + description: + name: source_span + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" + source: hosted + version: "1.9.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "3dac9aecf2c3991d09b9cdde4f98ded7b30804a88a0d7e4e7e1678e78d6b97f4" + url: "https://pub.dev" + source: hosted + version: "1.24.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + url: "https://pub.dev" + source: hosted + version: "0.5.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: "5138dbffb77b2289ecb12b81c11ba46036590b72a64a7a90d6ffb880f1a29e93" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + very_good_analysis: + dependency: "direct dev" + description: + name: very_good_analysis + sha256: ebc48c51db35beeeec8c414e32f7bd78e612bd7f5992ccb0d46e19edaeb40b08 + url: "https://pub.dev" + source: hosted + version: "4.0.0+1" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: f6deed8ed625c52864792459709183da231ebf66ff0cf09e69b573227c377efe + url: "https://pub.dev" + source: hosted + version: "11.3.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + url: "https://pub.dev" + source: hosted + version: "2.3.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + yaml: + dependency: "direct main" + description: + name: yaml + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" + source: hosted + version: "3.1.1" +sdks: + dart: ">=2.19.5 <3.0.0" diff --git a/packages/artifact_proxy/pubspec.yaml b/packages/artifact_proxy/pubspec.yaml new file mode 100644 index 00000000..4689c93e --- /dev/null +++ b/packages/artifact_proxy/pubspec.yaml @@ -0,0 +1,19 @@ +name: artifact_proxy +description: Intercept and proxy Flutter artifact requests. +version: 1.0.0 +repository: https://github.com/shorebirdtech/shorebird/ +publish_to: "none" + +environment: + sdk: ">=2.19.5 <3.0.0" + +dependencies: + collection: ^1.17.1 + shelf: ^1.4.0 + shelf_hotreload: ^1.4.0 + yaml: ^3.1.1 + +dev_dependencies: + mocktail: ^0.3.0 + test: ^1.19.2 + very_good_analysis: ^4.0.0 diff --git a/packages/artifact_proxy/test/artifact_proxy_test.dart b/packages/artifact_proxy/test/artifact_proxy_test.dart new file mode 100644 index 00000000..5bfba47a --- /dev/null +++ b/packages/artifact_proxy/test/artifact_proxy_test.dart @@ -0,0 +1,96 @@ +import 'dart:io'; + +import 'package:artifact_proxy/artifact_proxy.dart'; +import 'package:shelf/shelf.dart'; +import 'package:test/test.dart'; + +void main() { + const shorebirdEngineRevision = 'ff32625d5bda6d3eb2eb131e30a4c26ed4960002'; + const flutterEngineRevision = 'ec975089acb540fc60752606a3d3ba809dd1528b'; + const shorebirdStorageBucket = 'download.shorebird.dev'; + const config = { + 'engine_mappings': { + shorebirdEngineRevision: { + 'flutter_engine_revision': flutterEngineRevision, + 'shorebird_storage_bucket': shorebirdStorageBucket, + 'shorebird_artifact_overrides': [ + r'flutter_infra_release/flutter/$engine/android-x64-release/artifacts.zip' + ] + } + } + }; + + Request buildRequest(String path) { + return Request('GET', Uri.parse('http://localhost').replace(path: path)); + } + + Matcher isRedirectTo(String location) { + return isA() + .having((r) => r.statusCode, 'status code', HttpStatus.found) + .having((r) => r.headers['location'], 'location', location); + } + + group('artifactProxy', () { + test( + 'should proxy to Flutter artifacts ' + 'when no engine revision is detected', () async { + const path = 'path/with/no/revision/foo.zip'; + final handler = artifactProxyHandler(config: config); + final request = buildRequest(path); + final response = await handler(request); + expect( + response, + isRedirectTo('https://storage.googleapis.com/$path'), + ); + }); + + test( + 'should proxy to Shorebird artifacts ' + 'when an engine revision is detected', () async { + const path = + 'flutter_infra_release/flutter/$shorebirdEngineRevision/android-x64-release/artifacts.zip'; + final handler = artifactProxyHandler(config: config); + final request = buildRequest(path); + final response = await handler(request); + expect( + response, + isRedirectTo( + 'https://storage.googleapis.com/$shorebirdStorageBucket/$path', + ), + ); + }); + + test( + 'should proxy to Flutter ' + 'when no shorebird override is found', () async { + const path = + 'flutter_infra_release/flutter/$shorebirdEngineRevision/unknown/artifacts.zip'; + final handler = artifactProxyHandler(config: config); + final request = buildRequest(path); + final response = await handler(request); + expect( + response, + isRedirectTo( + 'https://storage.googleapis.com/${path.replaceFirst(shorebirdEngineRevision, flutterEngineRevision)}', + ), + ); + }); + + test( + 'should proxy to Flutter ' + 'when no shorebird override is found ' + 'and path is chrome infra', () async { + const path = + '/flutter_infra_release/cipd/flutter/web/canvaskit_bundle/+/ztaLvbs5GPmlAwUosC7VVp7EQnNVknRpNuKdv7vmzaAC'; + final handler = artifactProxyHandler(config: config); + final request = buildRequest(path); + final response = await handler(request); + expect( + response, + isRedirectTo( + 'https://chrome-infra-packages.appspot.com/dl/flutter/web/canvaskit_bundle/+/ztaLvbs5GPmlAwUosC7VVp7EQnNVknRpNuKdv7vmzaAC', + ), + ); + }); + }); +} diff --git a/packages/shorebird_cli/README.md b/packages/shorebird_cli/README.md index 6835b5c7..9a466693 100644 --- a/packages/shorebird_cli/README.md +++ b/packages/shorebird_cli/README.md @@ -316,7 +316,7 @@ shorebird channels create --name MyChannel **Sample** ``` -shorebird channels create --name MyChannel +shorebird channels create --name MyChannel 🚀 Ready to create a new channel!