diff --git a/.github/composite_actions/log_metric/action.yaml b/.github/composite_actions/log_metric/action.yaml index 4043545c78..98372e4e02 100644 --- a/.github/composite_actions/log_metric/action.yaml +++ b/.github/composite_actions/log_metric/action.yaml @@ -1,5 +1,5 @@ name: Log Metric -description: Log data point to a metric with the provided value. If the metric is not there, it will create one +description: Log data point to a metric with the provided value. If the metric is not there, it will create one. # To avoid 'Credentials could not be loaded' calling workflows must include: # permissions: # id-token: write @@ -7,24 +7,70 @@ description: Log data point to a metric with the provided value. If the metric i inputs: aws-region: required: true - description: The AWS region + description: The AWS region. role-to-assume: required: true - description: The role to assume in the STS session - metric-name: - description: Name of the metric to track in Cloudwatch. + description: The role to assume in the STS session. + github-token: required: true - value: - # Why we publish value 0 on success: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/publishingMetrics.html#publishingZero - description: Value of the metric to track in Cloudwatch. + description: Github token for requesting failing steps. + job-status: + description: Used to determine if we track success or failure. required: true - dimensions: - description: Dimensions of metric to track in Cloudwatch, in format dimensionName1=value,dimensionName2=value,... + job-identifier: + description: For differentiating jobs of a run. + required: true + + + # Global Metric Dimensions + testType: + description: canary, integration, unit testType. + required: true + category: + description: analytics, api, authenticator, etc. + required: true + workflowName: + description: The Github Action workflow.yaml file name. ie "AmplifyCanaries". + required: true + + # FlutterDart Workflows Metric Dimensions + framework: + description: flutter, dart. + required: false + flutterDartChannel: + description: beta, stable. + required: false + dartVersion: + description: 3, 2.19, 2.18, etc. + required: false + flutterVersion: + description: 3.10.6, 3.10.5, etc. + required: false + dartCompiler: + description: dart2js, ddc, dart, dart2wasm. required: false + + # Platform Workflows Metric Dimensions + platform: + description: android, ios, web, linux, windows. + required: false + platformVersion: + description: ios-14.5, ios-16, android-25-x86, etc. + required: false + runs: using: "composite" steps: + #- name: Exit if not scheduled + # shell: bash + # run: | + # if [ "${{ github.event_name }}" != "schedule" ]; then + # echo "This was not triggered by a schedule, skipping." + # echo "SKIP=true" >> $GITHUB_ENV + # fi + - name: Configure AWS credentials + if: env.SKIP != 'true' uses: aws-actions/configure-aws-credentials@04b98b3f9e85f563fb061be8751a0352327246b0 # 3.0.1 with: unset-current-credentials: true @@ -32,7 +78,50 @@ runs: aws-region: ${{ inputs.aws-region }} role-duration-seconds: 900 + - name: Change to Dart script directory + if: env.SKIP != 'true' + run: cd ./tool + shell: bash + + - name: Install Dart dependencies (args) + if: env.SKIP != 'true' + run: dart pub get + shell: bash + + - name: Get Failing Step + if: env.SKIP != 'true' && (${{ inputs.job-status }} == 'failure') + run: | + failing_step=$( \ + dart ./tool/get_failing_step.dart \ + --job-status "${{ inputs.job-status }}" \ + --substring "${{ inputs.job-identifier }}" \ + --github-token "${{ inputs.github-token }}" \ + --repo "${{ github.repository }}" \ + --run-id "${{ github.run_id }}" 2>&1) \ + echo FAILING_STEP=$failing_step >> $GITHUB_ENV + echo Failing Step was $failing_step + echo Failing Step was ${{ env.FAILING_STEP }} + echo Job Status was ${{ inputs.job-status }} + echo Job Status check was ${{ inputs.job-status == 'failure' }} + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + - name: Run Dart script - # Run a Dart script to put metric data. - run: dart ./tool/send_metric_data.dart ${{ inputs.metric-name }} ${{ inputs.value }} ${{ inputs.dimensions }} + if: env.SKIP != 'true' + run: | + dart ./tool/send_metric_data.dart \ + --metric-name="github_metric_1.0" \ + --is-failed="${{ inputs.job-status == 'failure' }}" \ + --test-type="${{ inputs.testType }}" \ + --category="${{ inputs.category }}" \ + --workflow-name="${{ inputs.workflowName }}" \ + --framework="${{ inputs.framework }}" \ + --flutter-dart-channel="${{ inputs.flutterDartChannel }}" \ + --dart-version="${{ inputs.dartVersion }}" \ + --flutter-version="${{ inputs.flutterVersion }}" \ + --dart-compiler="${{ inputs.dartCompiler }}" \ + --platform="${{ inputs.platform }}" \ + --platform-version="${{ inputs.platformVersion }}" \ + --failing-step="${{ env.FAILING_STEP }}" shell: bash diff --git a/.github/workflows/amplify_canaries.yaml b/.github/workflows/amplify_canaries.yaml index 6fadbaf584..5604790550 100644 --- a/.github/workflows/amplify_canaries.yaml +++ b/.github/workflows/amplify_canaries.yaml @@ -1,5 +1,13 @@ name: Amplify Canaries on: +# TEMP ENSURE PR TRIGGER + push: + branches: + - main + - stable + - feat/** + - fix/** + - test/** pull_request: paths: - ".github/workflows/amplify_canaries.yaml" @@ -55,24 +63,21 @@ jobs: - name: Build Canary (Android) run: build-support/build_canary.sh apk - - name: Log failing builds - if: ${{ failure() }} + - name: Log success/failure + if: always() uses: ./.github/composite_actions/log_metric with: role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} aws-region: ${{ secrets.AWS_REGION }} - metric-name: BuildCanaryTestFailure - value: 1 - dimensions: channel=${{ matrix.channel }} - - name: Log succeeding builds - if: ${{ success() }} - uses: ./.github/composite_actions/log_metric - with: - role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} - aws-region: ${{ secrets.AWS_REGION }} - metric-name: BuildCanaryTestFailure - value: 0 - dimensions: channel=${{ matrix.channel }} + github-token: ${{ secrets.GITHUB_TOKEN }} + job-status: ${{ job.status }} + job-identifier: build (${{ matrix.channel }}, ${{ matrix.flutter-version}}) + testType: canary + category: all + workflowName: amplify_canaries/build + framework: flutter + flutterDartChannel: ${{ matrix.channel }} + flutterVersion: ${{ matrix.flutter-version }} e2e-android: runs-on: @@ -130,24 +135,22 @@ jobs: # Perform a build to reduce startup time of `flutter test` and prevent timeout script: cd canaries && flutter build apk --debug && flutter test -d emulator-5554 integration_test/main_test.dart - - name: Log failing android runs - if: ${{ failure() }} + - name: Log success/failure + if: always() uses: ./.github/composite_actions/log_metric with: role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} aws-region: ${{ secrets.AWS_REGION }} - metric-name: E2ECanaryTestFailure - value: 1 - dimensions: channel=${{ matrix.channel }},platform=android - - name: Log succeeding android runs - if: ${{ success() }} - uses: ./.github/composite_actions/log_metric - with: - role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} - aws-region: ${{ secrets.AWS_REGION }} - metric-name: E2ECanaryTestFailure - value: 0 - dimensions: channel=${{ matrix.channel }},platform=android + github-token: ${{ secrets.GITHUB_TOKEN }} + job-status: ${{ job.status }} + job-identifier: e2e-android (${{ matrix.channel }}, ${{ matrix.flutter-version}}) + testType: canary + category: all + workflowName: amplify_canaries/e2e-android + framework: flutter + platform: android + flutterDartChannel: ${{ matrix.channel }} + flutterVersion: ${{ matrix.flutter-version }} e2e-ios: runs-on: macos-latest-xl @@ -208,21 +211,21 @@ jobs: flutter build ios --simulator --target=integration_test/main_test.dart flutter test -d test integration_test/main_test.dart --verbose - - name: Log failing ios runs - if: ${{ failure() }} - uses: ./.github/composite_actions/log_metric - with: - role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} - aws-region: ${{ secrets.AWS_REGION }} - metric-name: E2ECanaryTestFailure - value: 1 - dimensions: channel=${{ matrix.channel }},platform=ios - - name: Log succeeding ios runs - if: ${{ success() }} + - name: Log success/failure + if: always() uses: ./.github/composite_actions/log_metric with: role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} aws-region: ${{ secrets.AWS_REGION }} - metric-name: E2ECanaryTestFailure - value: 0 - dimensions: channel=${{ matrix.channel }},platform=ios + github-token: ${{ secrets.GITHUB_TOKEN }} + job-status: ${{ job.status }} + job-identifier: e2e-ios (${{ matrix.channel }}, ${{ matrix.flutter-version}}) + testType: canary + category: all + workflowName: amplify_canaries/e2e-ios + framework: flutter + platform: ios + platformVersion: ${{ matrix.ios-version }} + flutterDartChannel: ${{ matrix.channel }} + flutterVersion: ${{ matrix.flutter-version }} + diff --git a/.github/workflows/amplify_integration_tests.yaml b/.github/workflows/amplify_integration_tests.yaml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.github/workflows/dart_dart2js.yaml b/.github/workflows/dart_dart2js.yaml index 95f3906c1e..2f7ffe0ad9 100644 --- a/.github/workflows/dart_dart2js.yaml +++ b/.github/workflows/dart_dart2js.yaml @@ -82,3 +82,23 @@ jobs: if: "always() && steps.bootstrap.conclusion == 'success'" run: dart run build_runner test --release --delete-conflicting-outputs -- -p ${{ matrix.browser }} working-directory: ${{ inputs.working-directory }} + + - name: Log success/failure + if: always() + uses: ./.github/composite_actions/log_metric + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + job-status: ${{ job.status }} + job-identifier: ${{ matrix.sdk }}, ${{ matrix.browser }} + testType: unit test + category: ${{ inputs.working-directory }} + workflowName: ${{ inputs.package-name }}/dart_dart2js + framework: dart + flutterDartChannel: ${{ matrix.sdk }} + dartCompiler: dart2js + platform: web + # 'chrome/firefox' + platformVersion: $${{ matrix.browser }} + diff --git a/.github/workflows/dart_ddc.yaml b/.github/workflows/dart_ddc.yaml index d1a5103e0c..558eeaa25f 100644 --- a/.github/workflows/dart_ddc.yaml +++ b/.github/workflows/dart_ddc.yaml @@ -82,3 +82,22 @@ jobs: if: "always() && steps.bootstrap.conclusion == 'success'" run: dart run build_runner test --delete-conflicting-outputs -- -p ${{ matrix.browser }} working-directory: ${{ inputs.working-directory }} + + - name: Log success/failure + if: always() + uses: ./.github/composite_actions/log_metric + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + job-status: ${{ job.status }} + job-identifier: ${{ matrix.sdk }}, ${{ matrix.browser }} + testType: unit test + category: ${{ inputs.working-directory }} + workflowName: ${{ inputs.package-name }}/dart_ddc + framework: dart + flutterDartChannel: ${{ matrix.sdk }} + dartCompiler: dart2js + platform: web + # 'chrome/firefox' + platformVersion: $${{ matrix.browser }} diff --git a/.github/workflows/dart_native.yaml b/.github/workflows/dart_native.yaml index e1e109b046..48ccc5debb 100644 --- a/.github/workflows/dart_native.yaml +++ b/.github/workflows/dart_native.yaml @@ -88,3 +88,19 @@ jobs: if: "always() && steps.bootstrap.conclusion == 'success'" run: dart test --exclude-tags=build working-directory: ${{ inputs.working-directory }} + + - name: Log success/failure + if: always() + uses: ./.github/composite_actions/log_metric + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + job-status: ${{ job.status }} + job-identifier: ${{ matrix.os }} + testType: unit test + category: ${{ inputs.working-directory }} + workflowName: ${{ inputs.package-name }}/dart_native + framework: dart + flutterDartChannel: stable + dartCompiler: dart diff --git a/.github/workflows/dart_vm.yaml b/.github/workflows/dart_vm.yaml index 39a8d06257..4880a703ad 100644 --- a/.github/workflows/dart_vm.yaml +++ b/.github/workflows/dart_vm.yaml @@ -114,3 +114,20 @@ jobs: if: "always() && steps.bootstrap.conclusion == 'success' && steps.testCheck.outputs.hasTests == 'true' && matrix.sdk != 'stable'" run: dart test --exclude-tags=build working-directory: ${{ inputs.working-directory }} + + - name: Log success/failure + if: always() + uses: ./.github/composite_actions/log_metric + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + job-status: ${{ job.status }} + job-identifier: ${{ matrix.sdk }} + testType: unit test + category: ${{ inputs.working-directory }} + workflowName: ${{ inputs.package-name }}/dart_vm + framework: dart + flutterDartChannel: ${{ matrix.sdk }} + dartCompiler: dart + diff --git a/.github/workflows/flutter_android.yaml b/.github/workflows/flutter_android.yaml index 047238ca15..4556f65022 100644 --- a/.github/workflows/flutter_android.yaml +++ b/.github/workflows/flutter_android.yaml @@ -58,3 +58,19 @@ jobs: if: inputs.has-native-tests run: ./gradlew :"${{ inputs.package-name }}":testDebugUnitTest --stacktrace working-directory: ${{ inputs.example-directory }}/android + + - name: Log success/failure + if: always() + uses: ./.github/composite_actions/log_metric + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + job-status: ${{ job.status }} + job-identifier: ${{ matrix.channel }} + testType: unit test + category: ${{ inputs.working-directory }} + workflowName: ${{ inputs.package-name }}/flutter_android.test + framework: flutter + flutterDartChannel: ${{ matrix.channel }} + platform: android diff --git a/.github/workflows/flutter_ios.yaml b/.github/workflows/flutter_ios.yaml index e2c61fef00..784a131bc0 100644 --- a/.github/workflows/flutter_ios.yaml +++ b/.github/workflows/flutter_ios.yaml @@ -63,3 +63,19 @@ jobs: -scheme Runner \ -destination "$XCODEBUILD_DESTINATION" | xcpretty working-directory: ${{ inputs.example-directory }}/ios + + - name: Log success/failure + if: always() + uses: ./.github/composite_actions/log_metric + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + job-status: ${{ job.status }} + job-identifier: ${{ matrix.channel }} + testType: unit test + category: ${{ inputs.working-directory }} + workflowName: ${{ inputs.package-name }}/flutter_ios + framework: flutter + flutterDartChannel: ${{ matrix.channel }} + platform: ios diff --git a/.github/workflows/flutter_vm.yaml b/.github/workflows/flutter_vm.yaml index 6ceb34d023..4a6d4c1c20 100644 --- a/.github/workflows/flutter_vm.yaml +++ b/.github/workflows/flutter_vm.yaml @@ -85,3 +85,18 @@ jobs: if: "always() && steps.bootstrap.conclusion == 'success' && steps.testCheck.outputs.hasTests == 'true'" run: flutter test working-directory: ${{ inputs.working-directory }} + + - name: Log success/failure + if: always() + uses: ./.github/composite_actions/log_metric + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + job-status: ${{ job.status }} + job-identifier: ${{ matrix.channel }}, ${{ matrix.flutter-version }} + testType: unit test + category: ${{ inputs.working-directory }} + workflowName: ${{ inputs.package-name }}/flutter_vm + framework: flutter + flutterDartChannel: ${{ matrix.channel }} diff --git a/tool/get_failing_step.dart b/tool/get_failing_step.dart new file mode 100644 index 0000000000..7f80bc693e --- /dev/null +++ b/tool/get_failing_step.dart @@ -0,0 +1,83 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:convert'; +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:http/http.dart' as http; + +void main(List args) async { + final parser = ArgParser() + ..addOption('job-status') + ..addOption('substring') + ..addOption('github-token') + ..addOption('repo') + ..addOption('run-id'); + + final results = parser.parse(args); + + final jobStatus = results['job-status']; + + if (jobStatus != 'failure') { + print('Job status is not failure. Exiting.'); + exit(0); + } + + final substring = results['substring']?.trim(); + final githubToken = results['github-token']; + final repo = results['repo']; + final runId = results['run-id']; + + if ([substring, githubToken, repo, runId] + .any((e) => e == null || e.isEmpty)) { + print('All arguments are required.'); + exit(1); + } + + final headers = { + 'Authorization': 'token $githubToken', + 'Accept': 'application/vnd.github.v3+json' + }; + + final response = await http.get( + Uri.parse('https://api.github.com/repos/$repo/actions/runs/$runId/jobs'), + headers: headers, + ); + + if (response.statusCode != 200) { + print('Error fetching data from GitHub API.'); + exit(1); + } + + final jobsJson = json.decode(response.body) as Map; + final jobList = jobsJson['jobs'] as List; + + print('=== JOBS LIST ==='); + print(jobList); + + print('\n'); + print('\n'); + print('\n'); + + try { + final jobJson = + jobList.firstWhere((element) => element['name'].contains(substring)); + final steps = jobJson['steps'] as List; + + print('=== STEPS LIST ==='); + print(steps); + + final failingStep = steps.firstWhere( + (element) => element['conclusion'] == 'failure', + orElse: () => null); + + print('=== FAILING STEP ==='); + print(failingStep['name']); + } on Exception catch (_) { + // Return empty string if no job found or + print(' === IN EXCEPTION BLOCK ==='); + print(""); + exit(0); + } +} diff --git a/tool/pubspec.yaml b/tool/pubspec.yaml new file mode 100644 index 0000000000..f1f08790d5 --- /dev/null +++ b/tool/pubspec.yaml @@ -0,0 +1,9 @@ +name: send_metric_data +description: A Dart helper script to send metric data + +environment: + sdk: ^3.0.0 + +dependencies: + args: ^2.0.0 + http: ">=0.13.0 <2.0.0" diff --git a/tool/send_metric_data.dart b/tool/send_metric_data.dart index a74c81887a..97995ba4c9 100755 --- a/tool/send_metric_data.dart +++ b/tool/send_metric_data.dart @@ -3,36 +3,112 @@ import 'dart:io'; -/// Parse and send metric data using AWS CLI +import 'package:args/args.dart'; + +final testTypes = [ + 'canary', + 'integration', + 'unit test', +]; + +final frameworkTypes = [ + 'flutter', + 'dart', +]; + +final platformTypes = ['web', 'android', 'ios', 'linux', 'windows']; + void main(List args) { - final metricName = args[0]; - final value = args[1]; - final dimensions = args.length > 2 ? args[2] : ''; + final parser = ArgParser() + ..addOption('metric-name') + ..addOption('is-failed') + ..addOption('test-type') + ..addOption('category') + ..addOption('workflow-name') + ..addOption('framework') + ..addOption('flutter-dart-channel') + ..addOption('dart-version') + ..addOption('flutter-version') + ..addOption('dart-compiler') + ..addOption('platform') + ..addOption('platform-version') + ..addOption('failing-step'); - final metricNameTrimmed = metricName.trim(); - final valueTrimmed = value.trim(); - final dimensionsTrimmed = dimensions.trim(); + final results = parser.parse(args); - final metricNameRegex = RegExp(r'^[a-zA-Z0-9\ \_\-]+$'); - final valueRegex = RegExp(r'^[-+]?[0-9]+\.?[0-9]*$'); - final dimensionsRegex = RegExp(r'^([^=,]+=[^=,]+(?:,[^=,]+=[^=,]+)*)?$'); + final metricName = results['metric-name']?.trim(); + final isFailed = results['is-failed'] == 'true'; + final testType = results['test-type']?.trim(); + var category = results['category']?.trim(); + final workflowName = results['workflow-name']?.trim(); + final framework = results['framework']?.trim(); + final flutterDartChannel = results['flutter-dart-channel']?.trim(); + final dartVersion = results['dart-version']?.trim(); + final flutterVersion = results['flutter-version']?.trim(); + final dartCompiler = results['dart-compiler']?.trim(); + final platform = results['platform']?.trim(); + final platformVersion = results['platform-version']?.trim(); + final failingStep = results['failing-step']?.trim(); - if (!metricNameRegex.hasMatch(metricNameTrimmed)) { - print( - 'Metric name can only contain alphanumeric characters, space character, -, and _.'); + if (metricName.isEmpty) { + print('Must provide metric-name'); exit(1); } - if (!valueRegex.hasMatch(valueTrimmed)) { - print('Metric value must be a valid number'); + + final value = isFailed ? '1' : '0'; + + if (testType.isEmpty) { + print('Must provide test-type dimension'); + exit(1); + } else if (!testTypes.contains(testType)) { + print('test-type is not valid: $testType'); exit(1); } - if (!dimensionsRegex.hasMatch(dimensionsTrimmed)) { - print( - 'Dimensions must be empty or be in format string=string,string=string,...'); + + if (category.isEmpty) { + print('Must provide category dimension'); + exit(1); + } else if (category.contains('/')) { + // For working directory "packages/analytics/amplify_analytics_pinpoint" + category = category.split('/')[1]; + } else if (category.contains('_')) { + // For integration test scope "amplify_analytics_pinpoint_example" + category = category.split('_')[1]; + } + + if (workflowName.isEmpty) { + print('Must provide workflow-name dimension'); exit(1); } - final cloudArgs = [ + if (framework.isNotEmpty && !frameworkTypes.contains(framework)) { + print('Framework is not valid: $framework'); + exit(1); + } + + if (platform.isNotEmpty && !platformTypes.contains(platform)) { + print('Platform is not valid: $platform'); + exit(1); + } + + final dimensions = { + 'testType': testType, + 'category': category, + 'workflowName': workflowName, + if (framework.isNotEmpty) 'framework': framework, + if (flutterDartChannel.isNotEmpty) 'flutterDartChannel': flutterDartChannel, + if (dartVersion.isNotEmpty) 'dartVersion': dartVersion, + if (flutterVersion.isNotEmpty) 'flutterVersion': flutterVersion, + if (dartCompiler.isNotEmpty) 'dartCompiler': dartCompiler, + if (platform.isNotEmpty) 'platform': platform, + if (platformVersion.isNotEmpty) 'platformVersion': platformVersion, + if (failingStep.isNotEmpty) 'failingStep': failingStep, + }; + + final dimensionString = + dimensions.entries.map((e) => '${e.key}=${e.value}').join(','); + + final List cloudArgs = [ 'cloudwatch', 'put-metric-data', '--metric-name', @@ -41,12 +117,9 @@ void main(List args) { 'GithubCanaryApps', '--value', value, + '--dimension', + dimensionString, ]; - if (!dimensionsTrimmed.isEmpty) { - cloudArgs.add('--dimensions'); - cloudArgs.add(dimensionsTrimmed); - } - Process.runSync('aws', cloudArgs); }