#!/usr/bin/env ruby # frozen_string_literal: true require 'tempfile' require 'yaml' require 'open3' require 'pathname' args = ARGV.dup require_tracked = false require_clean = false args.delete_if do |arg| case arg when '--require-tracked' require_tracked = true true when '--require-clean' require_clean = true true when '--require-upload-ready' require_tracked = true require_clean = true true else false end end workflow_path = args.fetch(0) repo_root = File.expand_path('../..', File.dirname(workflow_path)) workflow_text = File.read(workflow_path) workflow = YAML.load_file(workflow_path) jobs = workflow.fetch('jobs') def fail!(message) warn "workflow contract failed: #{message}" exit 70 end def assert!(condition, message) fail!(message) unless condition end def job_runs(job) job.fetch('steps', []).each_with_object([]) do |step, runs| runs << step['run'] if step.key?('run') end end def upload_names(job) job.fetch('steps', []).each_with_object([]) do |step, names| next unless step['uses'].to_s.start_with?('actions/upload-artifact@') names << step.fetch('with', {}).fetch('name') end end def step_uses(job, action) job.fetch('steps', []).any? { |step| step['uses'].to_s == action } end def matrix_include(job) job.fetch('strategy').fetch('matrix').fetch('include') end def repo_path(repo_root, path) File.join(repo_root, path) end def read_repo_file(repo_root, path) File.read(repo_path(repo_root, path)) end def capture_command(*argv) stdout, _stderr, status = Open3.capture3(*argv) [status.success?, stdout.strip] end def git_toplevel_for(path) dir = File.directory?(path) ? path : File.dirname(path) success, stdout = capture_command('git', '-C', dir, 'rev-parse', '--show-toplevel') return nil unless success File.expand_path(stdout) end def relative_path(from, to) Pathname.new(File.expand_path(to)).relative_path_from(Pathname.new(File.expand_path(from))).to_s end def display_repo(repo_root, git_root) return '.' if File.expand_path(git_root) == File.expand_path(repo_root) relative_path(repo_root, git_root) end def required_git_repos(paths) paths.each_with_object({}) do |path, repos| git_root = git_toplevel_for(path) repos[git_root] = true unless git_root.nil? end.keys end scripts = [] jobs.each_value do |job| job.fetch('steps', []).each do |step| scripts << step['run'] if step.key?('run') end end scripts.each_with_index do |script, index| Tempfile.create(["open-shorebird-ci-run-#{index}", '.sh']) do |file| file.write(script.gsub(/\$\{\{[^}]+\}\}/, 'x')) file.flush system('bash', '-n', file.path, exception: true) end end required_files = %w[ .gitmodules README.md docs/CI.md docs/PLATFORM_TESTING.md docs/REPOSITORIES.md shorebird/README.md shorebird/OPEN_SOURCE_REPLACEMENTS.md shorebird/docs/account/api-keys/README.md shorebird/docs/code-push/troubleshooting/README.md shorebird/docs/getting-started/flutter-version/README.md scripts/assemble_artifact_mirror.sh scripts/android_runtime_patch_smoke.sh scripts/bootstrap_linux.sh scripts/bootstrap_macos.sh scripts/check_ci_capacity.sh scripts/free_ci_disk_linux.sh scripts/linux_runtime_patch_smoke.sh scripts/verify_open_infrastructure_defaults.sh scripts/safe_extract_tar.py scripts/sync_open_sources.sh scripts/validate_artifact_mirror.py scripts/validate_release_manifest.py scripts/verify_assemble_artifact_mirror.sh scripts/verify_artifact_mirror_validator.sh scripts/verify_artifact_mirror_workflow_assembly.sh scripts/verify_ci_workflow.rb scripts/verify_ci_workflow.sh scripts/verify_ci_capacity.sh scripts/verify_dart_sdk_args.sh scripts/verify_engine_args.sh scripts/verify_hosted_full_sdk_build.sh scripts/verify_ios_interpreter_route.sh scripts/verify_ios_interpreter_route_validator.sh scripts/verify_downloaded_release_artifacts.sh scripts/verify_powershell_open_defaults.sh scripts/verify_release_manifest.sh scripts/verify_sync_open_sources.sh scripts/verify_upload_readiness.sh scripts/verify_write_sha256.sh scripts/write_artifact_manifest.py scripts/write_release_manifest.py scripts/write_gclient.sh scripts/write_sha256.py scripts/write_sha256.sh shorebird/bin/shorebird.ps1 shorebird/third_party/flutter/bin/internal/shared.sh shorebird/packages/shorebird_cli/lib/src/cache.dart shorebird/packages/shorebird_cli/lib/src/commands/doctor_command.dart shorebird/packages/shorebird_cli/lib/src/network_checker.dart shorebird/packages/shorebird_cli/lib/src/shorebird_cli_command_runner.dart shorebird/packages/shorebird_cli/lib/src/shorebird_documentation.dart shorebird/packages/shorebird_cli/lib/src/shorebird_env.dart shorebird/packages/shorebird_cli/lib/src/shorebird_flutter.dart shorebird/packages/shorebird_cli/lib/src/shorebird_process.dart shorebird/packages/shorebird_cli/lib/src/shorebird_web_console.dart shorebird/packages/shorebird_code_push_client/lib/src/code_push_client.dart shorebird/packages/shorebird_code_push_protocol/tool/gen.dart shorebird/packages/shorebird_code_push_protocol/README.md shorebird-server/internal/api/handlers/openapi.yaml shorebird-server/internal/api/handlers/router.go shorebird/packages/artifact_proxy/lib/src/artifact_manifest_client.dart shorebird/packages/artifact_proxy/lib/src/artifact_proxy.dart shorebird/packages/artifact_proxy/lib/config.dart flutter/bin/internal/update_dart_sdk.ps1 flutter/bin/internal/update_dart_sdk.sh flutter/packages/flutter_tools/lib/src/cache.dart flutter/packages/flutter_tools/lib/src/http_host_validator.dart flutter/packages/flutter_tools/pubspec.yaml flutter/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt flutter/packages/flutter_tools/gradle/aar_init_script.gradle flutter/DEPS flutter/engine/src/flutter/build/dart/BUILD.gn flutter/dev/bots/post_process_docs.dart flutter/dev/bots/unpublish_package.dart flutter/dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/settings.gradle flutter/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/settings.gradle.kts flutter/dev/tools/create_api_docs.dart flutter/engine/src/flutter/build/zip_bundle.gni flutter/engine/src/flutter/runtime/dart_isolate.cc flutter/engine/src/flutter/lib/web_ui/dev/steps/copy_artifacts_step.dart flutter/packages/shorebird_tests/test/shorebird_tests.dart updater/library/src/config.rs ] missing_files = required_files.reject { |path| File.file?(repo_path(repo_root, path)) } assert!(missing_files.empty?, "missing CI support files: #{missing_files.join(', ')}") required_executables = required_files.select { |path| path.start_with?('scripts/') } missing_executable_bits = required_executables.reject do |path| File.executable?(repo_path(repo_root, path)) end assert!( missing_executable_bits.empty?, "missing executable bit on CI support scripts: #{missing_executable_bits.join(', ')}" ) required_git_paths = [workflow_path] + required_files.map { |path| repo_path(repo_root, path) } if require_tracked untracked_required_files = [] required_git_paths.each do |path| git_root = git_toplevel_for(path) display_path = path.start_with?(repo_root) ? relative_path(repo_root, path) : path if git_root.nil? untracked_required_files << "#{display_path}: not inside a git checkout" next end path_in_repo = relative_path(git_root, path) tracked, = capture_command( 'git', '-C', git_root, 'ls-files', '--error-unmatch', '--', path_in_repo ) next if tracked untracked_required_files << "#{display_path}: not tracked in #{display_repo(repo_root, git_root)}" end assert!( untracked_required_files.empty?, "required upload files are not tracked: #{untracked_required_files.join(', ')}" ) end if require_clean dirty_repos = [] required_git_repos(required_git_paths).each do |git_root| status_ok, status = capture_command('git', '-C', git_root, 'status', '--porcelain') unless status_ok dirty_repos << "#{display_repo(repo_root, git_root)}: unable to read git status" next end next if status.empty? dirty_repos << "#{display_repo(repo_root, git_root)}: has uncommitted changes" end assert!( dirty_repos.empty?, "required upload repositories are dirty: #{dirty_repos.join(', ')}" ) end readme = read_repo_file(repo_root, 'README.md') ci_doc = read_repo_file(repo_root, 'docs/CI.md') platform_doc = read_repo_file(repo_root, 'docs/PLATFORM_TESTING.md') repositories_doc = read_repo_file(repo_root, 'docs/REPOSITORIES.md') open_replacements_doc = read_repo_file(repo_root, 'shorebird/OPEN_SOURCE_REPLACEMENTS.md') shorebird_readme = read_repo_file(repo_root, 'shorebird/README.md') gitmodules = read_repo_file(repo_root, '.gitmodules') shorebird_flutter = read_repo_file( repo_root, 'shorebird/packages/shorebird_cli/lib/src/shorebird_flutter.dart' ) shorebird_env = read_repo_file( repo_root, 'shorebird/packages/shorebird_cli/lib/src/shorebird_env.dart' ) code_push_client = read_repo_file( repo_root, 'shorebird/packages/shorebird_code_push_client/lib/src/code_push_client.dart' ) code_push_protocol_gen = read_repo_file( repo_root, 'shorebird/packages/shorebird_code_push_protocol/tool/gen.dart' ) code_push_protocol_readme = read_repo_file( repo_root, 'shorebird/packages/shorebird_code_push_protocol/README.md' ) server_openapi = read_repo_file( repo_root, 'shorebird-server/internal/api/handlers/openapi.yaml' ) server_router = read_repo_file( repo_root, 'shorebird-server/internal/api/handlers/router.go' ) shorebird_cache = read_repo_file( repo_root, 'shorebird/packages/shorebird_cli/lib/src/cache.dart' ) shorebird_cli_command_runner = read_repo_file( repo_root, 'shorebird/packages/shorebird_cli/lib/src/shorebird_cli_command_runner.dart' ) shorebird_documentation = read_repo_file( repo_root, 'shorebird/packages/shorebird_cli/lib/src/shorebird_documentation.dart' ) doctor_command = read_repo_file( repo_root, 'shorebird/packages/shorebird_cli/lib/src/commands/doctor_command.dart' ) shorebird_process = read_repo_file( repo_root, 'shorebird/packages/shorebird_cli/lib/src/shorebird_process.dart' ) shorebird_powershell_launcher = read_repo_file(repo_root, 'shorebird/bin/shorebird.ps1') shorebird_shell_launcher = read_repo_file( repo_root, 'shorebird/third_party/flutter/bin/internal/shared.sh' ) network_checker = read_repo_file( repo_root, 'shorebird/packages/shorebird_cli/lib/src/network_checker.dart' ) shorebird_web_console = read_repo_file( repo_root, 'shorebird/packages/shorebird_cli/lib/src/shorebird_web_console.dart' ) updater_config = read_repo_file( repo_root, 'updater/library/src/config.rs' ) artifact_manifest_client = read_repo_file( repo_root, 'shorebird/packages/artifact_proxy/lib/src/artifact_manifest_client.dart' ) artifact_proxy = read_repo_file( repo_root, 'shorebird/packages/artifact_proxy/lib/src/artifact_proxy.dart' ) artifact_proxy_test = read_repo_file( repo_root, 'shorebird/packages/artifact_proxy/test/artifact_proxy_test.dart' ) artifact_proxy_config = read_repo_file( repo_root, 'shorebird/packages/artifact_proxy/lib/config.dart' ) write_gclient = read_repo_file(repo_root, 'scripts/write_gclient.sh') free_ci_disk_linux = read_repo_file(repo_root, 'scripts/free_ci_disk_linux.sh') check_ci_capacity = read_repo_file(repo_root, 'scripts/check_ci_capacity.sh') verify_ci_capacity = read_repo_file(repo_root, 'scripts/verify_ci_capacity.sh') flutter_tool_cache = read_repo_file( repo_root, 'flutter/packages/flutter_tools/lib/src/cache.dart' ) flutter_tools_pubspec = read_repo_file( repo_root, 'flutter/packages/flutter_tools/pubspec.yaml' ) flutter_http_host_validator = read_repo_file( repo_root, 'flutter/packages/flutter_tools/lib/src/http_host_validator.dart' ) flutter_gradle_constants = read_repo_file( repo_root, 'flutter/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt' ) flutter_aar_init_script = read_repo_file( repo_root, 'flutter/packages/flutter_tools/gradle/aar_init_script.gradle' ) flutter_deps = read_repo_file(repo_root, 'flutter/DEPS') flutter_post_process_docs = read_repo_file(repo_root, 'flutter/dev/bots/post_process_docs.dart') flutter_unpublish_package = read_repo_file(repo_root, 'flutter/dev/bots/unpublish_package.dart') flutter_android_host_app_settings = read_repo_file( repo_root, 'flutter/dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/settings.gradle' ) flutter_android_host_app_kts_settings = read_repo_file( repo_root, 'flutter/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/settings.gradle.kts' ) flutter_create_api_docs = read_repo_file(repo_root, 'flutter/dev/tools/create_api_docs.dart') flutter_zip_bundle = read_repo_file( repo_root, 'flutter/engine/src/flutter/build/zip_bundle.gni' ) flutter_dart_build = read_repo_file( repo_root, 'flutter/engine/src/flutter/build/dart/BUILD.gn' ) flutter_engine_archives_build = read_repo_file( repo_root, 'flutter/engine/src/flutter/build/archives/BUILD.gn' ) flutter_linux_build = read_repo_file( repo_root, 'flutter/engine/src/flutter/shell/platform/linux/BUILD.gn' ) flutter_android_build = read_repo_file( repo_root, 'flutter/engine/src/flutter/shell/platform/android/BUILD.gn' ) flutter_web_sdk_build = read_repo_file( repo_root, 'flutter/engine/src/flutter/web_sdk/BUILD.gn' ) flutter_ios_build = read_repo_file( repo_root, 'flutter/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn' ) flutter_macos_build = read_repo_file( repo_root, 'flutter/engine/src/flutter/shell/platform/darwin/macos/BUILD.gn' ) flutter_dart_isolate = read_repo_file( repo_root, 'flutter/engine/src/flutter/runtime/dart_isolate.cc' ) flutter_snapshot_build = read_repo_file( repo_root, 'flutter/engine/src/flutter/lib/snapshot/BUILD.gn' ) flutter_web_ui_copy_artifacts = read_repo_file( repo_root, 'flutter/engine/src/flutter/lib/web_ui/dev/steps/copy_artifacts_step.dart' ) flutter_shorebird_tests = read_repo_file( repo_root, 'flutter/packages/shorebird_tests/test/shorebird_tests.dart' ) flutter_update_dart_sdk_ps1 = read_repo_file( repo_root, 'flutter/bin/internal/update_dart_sdk.ps1' ) flutter_update_dart_sdk_sh = read_repo_file( repo_root, 'flutter/bin/internal/update_dart_sdk.sh' ) generated_artifact_manifest = IO.popen( [ 'python3', repo_path(repo_root, 'scripts/write_artifact_manifest.py'), '--flutter-engine-revision', 'flutter-base-revision', ], &:read ) assert!($?.success?, 'artifact manifest helper must run successfully') assert!( generated_artifact_manifest.include?("flutter_engine_revision: 'flutter-base-revision'") && generated_artifact_manifest.include?("storage_bucket: 'shorebird'") && generated_artifact_manifest.include?('flutter_infra_release/flutter/$engine/android-arm64-release/artifacts.zip') && generated_artifact_manifest.include?('flutter_infra_release/flutter/$engine/linux-x64-release/artifacts.zip') && generated_artifact_manifest.include?('flutter_infra_release/flutter/$engine/ios-release/artifacts.zip') && generated_artifact_manifest.include?('flutter_infra_release/flutter/$engine/flutter-web-sdk.zip') && generated_artifact_manifest.include?('flutter_infra_release/flutter/$engine/darwin-arm64-release/FlutterMacOS.framework.zip'), 'artifact manifest helper must emit open mirror overrides for CI-produced engine artifacts' ) generated_manifest_yaml = YAML.safe_load(generated_artifact_manifest) generated_artifact_overrides = generated_manifest_yaml.fetch('artifact_overrides') missing_mirror_paths = generated_artifact_overrides.map do |artifact_path| "mirror/shorebird/#{artifact_path.gsub('$engine', '${engine_revision}')}" end.reject { |mirror_path| workflow_text.include?(mirror_path) } assert!( missing_mirror_paths.empty?, "workflow engine artifacts must package every generated mirror override: #{missing_mirror_paths.join(', ')}" ) assert!( workflow_text.scan('test -f "$mirror_root').length >= generated_artifact_overrides.length, 'workflow engine artifact packaging must verify mirror-ready files exist before uploading' ) closed_runtime_endpoint_patterns = %w[ download.shorebird.dev api.shorebird.dev auth.shorebird.dev console.shorebird.dev cdn.shorebird.cloud ] runtime_endpoint_matches = Dir[ repo_path(repo_root, 'shorebird/packages/shorebird_cli/lib/**/*.dart'), repo_path(repo_root, 'shorebird/packages/shorebird_code_push_client/lib/**/*.dart'), repo_path(repo_root, 'shorebird/packages/artifact_proxy/lib/**/*.dart'), repo_path(repo_root, 'updater/library/src/**/*.rs'), repo_path(repo_root, 'flutter/packages/flutter_tools/lib/src/cache.dart'), repo_path(repo_root, 'flutter/packages/flutter_tools/lib/src/http_host_validator.dart'), repo_path(repo_root, 'flutter/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt'), repo_path(repo_root, 'flutter/packages/flutter_tools/gradle/aar_init_script.gradle'), repo_path(repo_root, 'flutter/DEPS'), repo_path(repo_root, 'flutter/dev/bots/post_process_docs.dart'), repo_path(repo_root, 'flutter/dev/bots/unpublish_package.dart'), repo_path(repo_root, 'flutter/dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/settings.gradle'), repo_path(repo_root, 'flutter/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/settings.gradle.kts'), repo_path(repo_root, 'flutter/dev/tools/create_api_docs.dart'), repo_path(repo_root, 'flutter/bin/internal/update_dart_sdk.ps1'), repo_path(repo_root, 'flutter/bin/internal/update_dart_sdk.sh'), repo_path(repo_root, 'flutter/engine/src/flutter/build/zip_bundle.gni'), repo_path(repo_root, 'flutter/engine/src/flutter/lib/web_ui/dev/steps/copy_artifacts_step.dart'), repo_path(repo_root, 'flutter/packages/shorebird_tests/test/shorebird_tests.dart') ].flat_map do |path| text = File.read(path) closed_runtime_endpoint_patterns.each_with_object([]) do |pattern, matches| matches << "#{path.delete_prefix("#{repo_root}/")}: #{pattern}" if text.include?(pattern) end end assert!( runtime_endpoint_matches.empty?, "runtime code must not hard-code official hosted endpoints: #{runtime_endpoint_matches.join(', ')}" ) official_support_link_patterns = %w[ docs.shorebird.dev app.codecov.io/gh/shorebirdtech/shorebird codecov.io/gh/shorebirdtech/shorebird discord.gg/shorebird handbook.shorebird.dev producthunt.com/posts/shorebird-code-push shorebird.dev/privacy contact@shorebird.dev github.com/shorebirdtech/shorebird github.com/shorebirdtech/updater github.com/shorebirdtech/flutter git@github.com:shorebirdtech/shorebird.git ] official_support_link_matches = Dir[ repo_path(repo_root, 'shorebird/README.md'), repo_path(repo_root, 'shorebird/bin/shorebird.ps1'), repo_path(repo_root, 'shorebird/third_party/flutter/bin/internal/shared.sh'), repo_path(repo_root, 'shorebird/packages/shorebird_cli/lib/src/**/*.dart'), repo_path(repo_root, 'shorebird/packages/shorebird_code_push_client/lib/**/*.dart'), repo_path(repo_root, 'shorebird/packages/artifact_proxy/lib/**/*.dart'), repo_path(repo_root, 'updater/library/src/**/*.rs'), repo_path(repo_root, 'flutter/packages/flutter_tools/pubspec.yaml'), repo_path(repo_root, 'flutter/engine/src/flutter/lib/web_ui/dev/steps/copy_artifacts_step.dart') ].flat_map do |path| text = File.read(path) official_support_link_patterns.each_with_object([]) do |pattern, matches| matches << "#{path.delete_prefix("#{repo_root}/")}: #{pattern}" if text.include?(pattern) end end assert!( official_support_link_matches.empty?, "shorebird CLI source must direct users to open docs/issues, not official Shorebird support links: #{official_support_link_matches.join(', ')}" ) official_package_metadata_matches = Dir[ repo_path(repo_root, 'shorebird/packages/*/pubspec.yaml') ].flat_map do |path| text = File.read(path) [ 'github.com/shorebirdtech/shorebird', 'homepage: https://shorebird.dev', 'repository: https://shorebird.dev', ].each_with_object([]) do |pattern, matches| matches << "#{path.delete_prefix("#{repo_root}/")}: #{pattern}" if text.include?(pattern) end end assert!( official_package_metadata_matches.empty?, "package metadata must point at the open workspace, not official Shorebird metadata: #{official_package_metadata_matches.join(', ')}" ) expected_submodules = { 'dart-sdk-new' => ['https://git.tonycloud.org/dart-lang/sdk.git', 'tonycloud/dev'], 'depot_tools' => ['https://chromium.googlesource.com/chromium/tools/depot_tools.git', 'main'], 'flutter' => ['https://git.tonycloud.org/flutter/flutter.git', 'shorebird/dev'], 'shorebird' => ['https://git.tonycloud.org/flutter/shorebird.git', 'main'], 'shorebird-server' => ['https://git.tonycloud.org/flutter/shorebird-server.git', 'main'], 'updater' => ['https://git.tonycloud.org/flutter/shorebird-updater.git', 'main'], } expected_submodules.each do |path, (url, branch)| assert!( gitmodules.include?("[submodule \"#{path}\"]") && gitmodules.include?("path = #{path}") && gitmodules.include?("url = #{url}") && gitmodules.include?("branch = #{branch}"), ".gitmodules must pin open HTTPS submodule #{path} to #{url} on #{branch}" ) assert!( repositories_doc.include?("| `#{path}` | `#{url}` | `#{branch}` |"), "docs/REPOSITORIES.md must document submodule #{path}" ) end forbidden_gitmodule_fragments = [ 'git@', 'file://', '/Users/', '../', 'github.com/shorebirdtech/', 'github.com/shorebirdtech', ] forbidden_gitmodule_fragments.each do |fragment| assert!( !gitmodules.include?(fragment), ".gitmodules must not contain non-open or non-portable submodule URL fragment #{fragment}" ) end assert!( write_gclient.include?('"url": "https://git.tonycloud.org/dart-lang/sdk.git"') && write_gclient.include?('"url": "https://git.tonycloud.org/flutter/flutter.git"') && !write_gclient.include?('github.com/shorebirdtech'), 'gclient generation must use the open Dart/Flutter forks and avoid official Shorebird remotes' ) { 'README.md' => [ '.github/workflows/open-shorebird-ci.yml', 'full_sdk_build=true', 'run_runtime_smokes=true', 'SHOREBIRD_FLUTTER_GIT_URL', 'SHOREBIRD_FLUTTER_STORAGE_BASE_URL', 'scripts/assemble_artifact_mirror.sh', 'scripts/verify_upload_readiness.sh', 'open-shorebird-artifact-mirror', 'open-shorebird-release-manifest', 'http://localhost:8080', 'http://localhost:8080/artifacts', 'http://localhost:8080/download.flutter.io', 'DART_DYNAMIC_MODULES', 'artifacts_manifest.yaml', ], 'docs/CI.md' => [ 'custom-dart-sdk-linux-x64', 'custom-dart-sdk-macos-arm64', 'ios-interpreter-engine', 'scripts/write_sha256.sh', 'scripts/verify_upload_readiness.sh', 'SHOREBIRD_FLUTTER_GIT_URL', 'SHOREBIRD_FLUTTER_STORAGE_BASE_URL', 'scripts/assemble_artifact_mirror.sh', 'open-shorebird-artifact-mirror', 'open-shorebird-release-manifest', 'http://localhost:8080', 'http://localhost:8080/artifacts', 'http://localhost:8080/download.flutter.io', 'http://localhost:8080/openapi.yaml', 'mirror-metadata', 'artifacts_manifest.yaml', 'verify_artifact_mirror_workflow_assembly.sh', ], 'docs/PLATFORM_TESTING.md' => [ 'license:pro', 'DART_DYNAMIC_MODULES=false', 'real iPad', '/openapi.yaml', ], 'docs/REPOSITORIES.md' => [ 'https://git.tonycloud.org/dart-lang/sdk.git', 'https://git.tonycloud.org/flutter/flutter.git', 'https://git.tonycloud.org/flutter/shorebird.git', 'https://git.tonycloud.org/flutter/shorebird-server.git', 'https://git.tonycloud.org/flutter/shorebird-updater.git', 'Only top-level project repositories are submodules', ], 'shorebird/OPEN_SOURCE_REPLACEMENTS.md' => [ 'Local forks/submodules are the source of truth', './scripts/sync_open_sources.sh', 'http://localhost:8080', 'http://localhost:8080/openapi.yaml', 'http://localhost:8080/artifacts', 'http://localhost:8080/download.flutter.io', 'assemble_artifact_mirror.sh', 'open-shorebird-artifact-mirror', 'open-shorebird-release-manifest', 'DART_DYNAMIC_MODULES=false', 'verify_ci_workflow.sh', ], 'shorebird/README.md' => [ 'OPEN_SOURCE_REPLACEMENTS.md', '.github/workflows/open-shorebird-ci.yml', 'self-hosted services', 'custom Dart SDK', ], 'shorebird/docs/account/api-keys/README.md' => [ 'SHOREBIRD_TOKEN', 'http://localhost:8080/auth', ], 'shorebird/docs/code-push/troubleshooting/README.md' => [ 'SHOREBIRD_ARTIFACT_BASE_URL', 'SHOREBIRD_FLUTTER_STORAGE_BASE_URL', 'Asset changes are not part of a Dart code patch', ], 'shorebird/docs/getting-started/flutter-version/README.md' => [ 'https://git.tonycloud.org/flutter/flutter.git', 'http://localhost:8080/artifacts', ], }.each do |path, required_texts| text = { 'README.md' => readme, 'docs/CI.md' => ci_doc, 'docs/PLATFORM_TESTING.md' => platform_doc, 'docs/REPOSITORIES.md' => repositories_doc, 'shorebird/README.md' => shorebird_readme, 'shorebird/OPEN_SOURCE_REPLACEMENTS.md' => open_replacements_doc, }.fetch(path) { read_repo_file(repo_root, path) } missing_texts = required_texts.reject { |required_text| text.include?(required_text) } assert!(missing_texts.empty?, "#{path} missing required text: #{missing_texts.join(', ')}") end assert!( shorebird_flutter.include?('SHOREBIRD_FLUTTER_GIT_URL') && shorebird_flutter.include?('defaultFlutterGitUrl') && shorebird_flutter.include?('https://git.tonycloud.org/flutter/flutter.git'), 'shorebird Flutter installer must default to the open fork and allow SHOREBIRD_FLUTTER_GIT_URL override' ) assert!( shorebird_documentation.include?('openShorebirdRepositoryUrl') && shorebird_documentation.include?('https://git.tonycloud.org/flutter/shorebird') && shorebird_documentation.include?(%q{openShorebirdIssueUrl = '$openShorebirdRepositoryUrl/issues/new'}) && shorebird_documentation.include?(%q{docsUrl = '$openShorebirdRepositoryUrl/src/branch/main/docs'}) && !shorebird_documentation.include?('docs.shorebird.dev') && !shorebird_documentation.include?('github.com/shorebirdtech/shorebird'), 'shorebird documentation links must point to the open repository docs and issue tracker' ) { 'shorebird_cli_command_runner.dart' => shorebird_cli_command_runner, 'doctor_command.dart' => doctor_command, }.each do |path, text| assert!( text.include?('https://git.tonycloud.org/flutter/shorebird.git') && !text.include?('git@github.com:shorebirdtech/shorebird.git'), "#{path} must print the open Shorebird fork in user-visible version banners" ) end assert!( !shorebird_flutter.include?('github.com/shorebirdtech/flutter.git'), 'shorebird Flutter installer must not clone the closed official Shorebird Flutter fork' ) assert!( shorebird_env.include?("defaultHostedUrl = 'http://localhost:8080'") && shorebird_env.include?("defaultAuthServiceUrl = '\$defaultHostedUrl/auth'") && shorebird_env.include?("defaultJwtIssuer = 'shorebird-auth'"), 'shorebird environment must default to the open self-hosted server' ) assert!( !shorebird_env.include?('https://auth.shorebird.dev'), 'shorebird environment must not default to the official hosted auth service' ) assert!( code_push_client.include?("defaultHostedUri = Uri.parse('http://localhost:8080')") && !code_push_client.include?("Uri.https('api.shorebird.dev')"), 'code push client must default to the open self-hosted server, not api.shorebird.dev' ) assert!( code_push_protocol_gen.include?('http://localhost:8080/openapi.yaml') && code_push_protocol_gen.include?('../shorebird-server/internal/api/handlers/openapi.yaml') && code_push_protocol_gen.include?('Do not regenerate this package from Shorebird') && !code_push_protocol_gen.include?('api.shorebird.dev') && code_push_protocol_readme.include?('http://localhost:8080/openapi.yaml') && code_push_protocol_readme.include?('../shorebird-server/internal/api/handlers/openapi.yaml') && code_push_protocol_readme.include?("Shorebird's hosted API") && !code_push_protocol_readme.include?('api.shorebird.dev'), 'code push protocol regeneration docs must require an open/self-hosted spec, not the hosted Shorebird API' ) assert!( server_openapi.include?('openapi: 3.1.0') && server_openapi.include?('/openapi.yaml:') && server_openapi.include?('/api/v1/openapi.yaml:') && server_openapi.include?('/api/v1/patches/check:'), 'self-hosted server must include a checked OpenAPI contract for open clients' ) assert!( server_router.include?('go:embed openapi.yaml') && server_router.include?('serveOpenAPISpec') && server_router.include?('r.Get("/openapi.yaml", serveOpenAPISpec)') && server_router.include?('r.Get("/api/v1/openapi.yaml", serveOpenAPISpec)'), 'self-hosted server must serve the checked OpenAPI contract' ) assert!( shorebird_cache.include?('SHOREBIRD_ENABLE_LEGACY_AOT_TOOLS') && shorebird_cache.include?('legacyAotToolsEnabled') && shorebird_cache.include?('registerArtifact(AotToolsArtifact'), 'cache must gate legacy aot-tools behind an explicit environment variable' ) assert!( shorebird_cache.include?("defaultArtifactBaseUrl = 'http://localhost:8080/artifacts'") && !shorebird_cache.include?('https://storage.googleapis.com') && !shorebird_cache.include?('download.shorebird.dev'), 'cache must default artifact downloads to the open local mirror, not Shorebird hosted storage' ) assert!( shorebird_process.include?('defaultFlutterStorageBaseUrl') && shorebird_process.include?('${ShorebirdEnv.defaultHostedUrl}/download.flutter.io') && !shorebird_process.include?('https://download.shorebird.dev'), 'Flutter process environment must default to the open local Flutter artifact mirror' ) { 'shorebird/bin/shorebird.ps1' => shorebird_powershell_launcher, 'shorebird/third_party/flutter/bin/internal/shared.sh' => shorebird_shell_launcher, }.each do |path, text| assert!( text.include?('SHOREBIRD_FLUTTER_GIT_URL') && text.include?('https://git.tonycloud.org/flutter/flutter.git') && text.include?('SHOREBIRD_FLUTTER_STORAGE_BASE_URL') && text.include?('FLUTTER_STORAGE_BASE_URL') && text.include?('http://localhost:8080/download.flutter.io') && !text.include?('github.com/shorebirdtech/flutter.git') && !text.include?('https://download.shorebird.dev'), "#{path} must bootstrap from open Flutter/artifact defaults with environment overrides" ) end assert!( shorebird_shell_launcher.include?('https://git.tonycloud.org/flutter/shorebird') && !shorebird_shell_launcher.include?('github.com/shorebirdtech/shorebird'), 'shell launcher missing-clone guidance must point at the open Shorebird repository' ) assert!( network_checker.include?('shorebirdEnv.hostedUri') && network_checker.include?('shorebirdEnv.authServiceUri') && !network_checker.include?('api.shorebird.dev') && !network_checker.include?('console.shorebird.dev') && !network_checker.include?('cdn.shorebird.cloud'), 'network checker must inspect self-hosted endpoints instead of fixed Shorebird hosted URLs' ) assert!( shorebird_web_console.include?('shorebirdEnv.hostedUri') && !shorebird_web_console.include?('console.shorebird.dev'), 'web console links must use the configured self-hosted URL' ) assert!( updater_config.include?('const DEFAULT_BASE_URL: &str = "http://localhost:8080";') && !updater_config.include?('https://api.shorebird.dev'), 'updater runtime must default to the open self-hosted server' ) assert!( artifact_manifest_client.include?('defaultManifestBaseUri') && artifact_manifest_client.include?('http://localhost:8080/artifacts') && !artifact_manifest_client.include?('download.shorebird.dev'), 'artifact manifest client must default to the open artifact mirror' ) assert!( artifact_proxy.include?('defaultShorebirdArtifactBaseUri') && artifact_proxy.include?('http://localhost:8080/artifacts') && !artifact_proxy.include?('download.shorebird.dev') && !artifact_proxy.include?('docs.shorebird.dev') && !artifact_proxy.include?('shorebird.dev/contact'), 'artifact proxy must default Shorebird-specific redirects to the open artifact mirror' ) assert!( artifact_proxy_config.include?('linux-x64-release\/artifacts\.zip') && artifact_proxy_config.include?('ios-release\/artifacts\.zip') && artifact_proxy_config.include?('flutter-web-sdk\.zip') && artifact_proxy_config.include?('flutter_patched_sdk_product\.zip') && artifact_proxy_config.include?('darwin-arm64-release\/FlutterMacOS\.framework\.zip'), 'artifact proxy must recognize the generated artifact paths produced by open CI' ) generated_artifact_overrides.each do |artifact_path| expected_proxy_pattern = artifact_path .gsub('/', '\\/') .gsub('.', '\\.') .gsub('$engine', '(.*)') assert!( artifact_proxy_config.include?(expected_proxy_pattern), "artifact proxy config must recognize generated override #{artifact_path}" ) assert!( artifact_proxy_test.include?(artifact_path), "artifact proxy tests must cover generated override #{artifact_path}" ) end assert!( artifact_proxy_test.include?('ios-release/artifacts.zip') && artifact_proxy_test.include?('flutter-web-sdk.zip') && artifact_proxy_test.include?('flutter_patched_sdk_product.zip'), 'artifact proxy tests must cover iOS, web, and patched SDK mirror redirects' ) assert!( flutter_tool_cache.include?("kOpenFlutterStorageUrl = 'http://localhost:8080/download.flutter.io'") && !flutter_tool_cache.include?('download.shorebird.dev') && !flutter_tool_cache.include?('kShorebirdStorageUrl'), 'Flutter tool cache must default engine artifact downloads to the open local Flutter mirror' ) assert!( flutter_http_host_validator.include?("kCloudHost = 'http://localhost:8080/download.flutter.io/'") && !flutter_http_host_validator.include?('download.shorebird.dev'), 'Flutter doctor network validator must check the open local Flutter mirror by default' ) { 'flutter/bin/internal/update_dart_sdk.ps1' => flutter_update_dart_sdk_ps1, 'flutter/bin/internal/update_dart_sdk.sh' => flutter_update_dart_sdk_sh, }.each do |path, text| assert!( text.include?('FLUTTER_STORAGE_BASE_URL') && text.include?('http://localhost:8080/download.flutter.io') && !text.include?('download.shorebird.dev'), "#{path} must download Dart SDK archives from FLUTTER_STORAGE_BASE_URL with the open local Flutter mirror fallback" ) end assert!( flutter_gradle_constants.include?('DEFAULT_MAVEN_HOST = "http://localhost:8080/download.flutter.io"') && !flutter_gradle_constants.include?('download.shorebird.dev') && flutter_aar_init_script.include?('?: "http://localhost:8080/download.flutter.io"') && !flutter_aar_init_script.include?('download.shorebird.dev'), 'Flutter Gradle plugin defaults must use the open local Flutter mirror' ) assert!( flutter_deps.include?('"dart_sdk_git": "https://git.tonycloud.org/dart-lang/sdk.git"') && flutter_deps.include?('"updater_git": "https://git.tonycloud.org/flutter/shorebird-updater.git"') && !flutter_deps.include?('git@github.com:shorebirdtech/dart-sdk.git') && !flutter_deps.include?('github.com/shorebirdtech/updater.git') && !flutter_deps.include?('shorebird-dart-sdk-prebuilt') && !flutter_deps.include?('shorebirdtech/_build_engine'), 'Flutter DEPS must point Dart/updater dependencies at open remotes and avoid Shorebird private prebuilt buckets' ) assert!( flutter_android_host_app_settings.include?('System.getenv("FLUTTER_STORAGE_BASE_URL") ?: "http://localhost:8080"') && flutter_android_host_app_settings.include?('$flutterStorageUrl/download.flutter.io') && !flutter_android_host_app_settings.include?('download.shorebird.dev') && flutter_android_host_app_kts_settings.include?('System.getenv("FLUTTER_STORAGE_BASE_URL") ?: "http://localhost:8080"') && flutter_android_host_app_kts_settings.include?('$flutterStorageUrl/download.flutter.io') && !flutter_android_host_app_kts_settings.include?('download.shorebird.dev'), 'Flutter Android host-app integration fixtures must default to the open local Flutter mirror' ) assert!( flutter_create_api_docs.include?("Platform.environment['FLUTTER_STORAGE_BASE_URL']") && flutter_create_api_docs.include?('http://localhost:8080/download.flutter.io') && !flutter_create_api_docs.include?('download.shorebird.dev') && flutter_post_process_docs.include?("Platform.environment['FLUTTER_STORAGE_BASE_URL']") && flutter_post_process_docs.include?('http://localhost:8080/download.flutter.io') && !flutter_post_process_docs.include?('download.shorebird.dev') && flutter_unpublish_package.include?('http://localhost:8080/download.flutter.io/flutter_infra_release') && !flutter_unpublish_package.include?('download.shorebird.dev') && flutter_shorebird_tests.include?("'FLUTTER_STORAGE_BASE_URL': 'http://localhost:8080/download.flutter.io'") && !flutter_shorebird_tests.include?('download.shorebird.dev'), 'Flutter docs/test artifact paths must default to the open local Flutter mirror' ) assert!( flutter_zip_bundle.include?('http://localhost:8080/download.flutter.io/flutter_infra_release/flutter/$engine_version/sky_engine.zip') && !flutter_zip_bundle.include?('download.shorebird.dev'), 'engine zip bundle metadata must use the open local Flutter mirror' ) assert!( flutter_dart_build.include?('import("$dart_src/build/dart/copy_tree.gni")') && flutter_dart_build.include?('copy_tree("copy_dart_sdk")') && flutter_dart_build.include?('source = prebuilt_dart_sdk') && flutter_dart_build.include?('dest = "$root_out_dir/dart-sdk"') && !flutter_dart_build.include?('copy_trees('), 'engine Dart SDK BUILD.gn must use the Dart SDK copy_tree template available in this checkout' ) assert!( flutter_dart_isolate.include?('#include "flutter/shell/common/shorebird/updater.h"') && flutter_dart_isolate.include?('std::string GetYamlValue(') && flutter_dart_isolate.include?('return UnquoteYamlValue(GetYamlValue(config.yaml_config, key));') && !flutter_dart_isolate.include?('#include "flutter/shell/common/shorebird/shorebird.h"'), 'dart_isolate.cc must avoid depending on the full Shorebird wrapper target so GN header checks pass' ) assert!( flutter_web_ui_copy_artifacts.include?("io.Platform.environment['FLUTTER_STORAGE_BASE_URL']") && flutter_web_ui_copy_artifacts.include?('http://localhost:8080/download.flutter.io') && flutter_web_ui_copy_artifacts.include?('storageBaseUri.resolve') && !flutter_web_ui_copy_artifacts.include?('download.shorebird.dev'), 'web UI artifact downloads must use FLUTTER_STORAGE_BASE_URL with the open local Flutter mirror fallback' ) assert!( flutter_tools_pubspec.include?('https://git.tonycloud.org/flutter/shorebird.git') && !flutter_tools_pubspec.include?('https://github.com/shorebirdtech/shorebird.git'), 'Flutter tools pubspec must fetch Shorebird build-trace code from the open fork' ) assert!( flutter_engine_archives_build.include?('zip_bundle("artifacts")') && flutter_engine_archives_build.include?('output = "$prefix/artifacts.zip"') && flutter_engine_archives_build.include?('zip_bundle("flutter_patched_sdk")') && flutter_engine_archives_build.include?('output = "flutter_patched_sdk${file_suffix}.zip"'), 'engine archive BUILD.gn must still define artifacts.zip and flutter_patched_sdk_product.zip targets used by CI' ) assert!( flutter_linux_build.include?('zip_bundle("flutter_gtk")') && flutter_linux_build.include?('output = "${prefix}${full_target_platform_name}-flutter-gtk.zip"'), 'Linux engine BUILD.gn must still define the flutter_gtk archive used by CI' ) assert!( flutter_android_build.include?('zip_bundle("android_symbols")') && flutter_android_build.include?('output = "$android_zip_archive_dir/symbols.zip"') && flutter_android_build.include?('zip_bundle("flutter_jar_zip")') && flutter_android_build.include?('output = "$android_zip_archive_dir/artifacts.zip"') && flutter_android_build.include?('zip_bundle("gen_snapshot")') && flutter_android_build.include?('zip_bundle("analyze_snapshot")'), 'Android engine BUILD.gn must still define symbols/artifacts/gen_snapshot/analyze_snapshot archives used by CI' ) assert!( flutter_web_sdk_build.include?('zip_bundle_from_file("flutter_web_sdk_archive")') && flutter_web_sdk_build.include?('output = "flutter-web-sdk.zip"'), 'web SDK BUILD.gn must still define flutter-web-sdk.zip used by CI' ) assert!( flutter_ios_build.include?('action("flutter_framework")') && flutter_ios_build.include?('outputs = [ "$root_out_dir/Flutter.xcframework" ]'), 'iOS engine BUILD.gn must still define Flutter.xcframework used by CI' ) assert!( flutter_macos_build.include?('zip_bundle("zip_macos_flutter_framework")') && flutter_macos_build.include?('output = "${prefix}FlutterMacOS.framework.zip"'), 'macOS engine BUILD.gn must still define FlutterMacOS.framework.zip used by CI' ) assert!( flutter_snapshot_build.include?('action("create_macos_gen_snapshots")') && flutter_snapshot_build.include?('action("create_macos_analyze_snapshots")'), 'snapshot BUILD.gn must still define macOS/iOS host snapshotter targets used by CI' ) assert!( !shorebird_cache.include?('aot-tools.dill artifact.\n/// Used for linking and generating optimized AOT snapshots.'), 'cache comments must not describe legacy closed aot-tools as a default optimized AOT path' ) platform_test_common = read_repo_file(repo_root, 'scripts/platform_test_common.sh') bootstrap_linux = read_repo_file(repo_root, 'scripts/bootstrap_linux.sh') bootstrap_macos = read_repo_file(repo_root, 'scripts/bootstrap_macos.sh') sync_open_sources = read_repo_file(repo_root, 'scripts/sync_open_sources.sh') verify_sync_open_sources = read_repo_file( repo_root, 'scripts/verify_sync_open_sources.sh' ) verify_open_infrastructure_defaults = read_repo_file( repo_root, 'scripts/verify_open_infrastructure_defaults.sh' ) verify_powershell_open_defaults = read_repo_file( repo_root, 'scripts/verify_powershell_open_defaults.sh' ) android_runtime_smoke = read_repo_file(repo_root, 'scripts/android_runtime_patch_smoke.sh') verify_ios_interpreter_route = read_repo_file( repo_root, 'scripts/verify_ios_interpreter_route.sh' ) verify_ios_interpreter_route_validator = read_repo_file( repo_root, 'scripts/verify_ios_interpreter_route_validator.sh' ) verify_engine_args = read_repo_file(repo_root, 'scripts/verify_engine_args.sh') verify_dart_sdk_args = read_repo_file(repo_root, 'scripts/verify_dart_sdk_args.sh') assert!( bootstrap_linux.include?('exec "$ROOT/scripts/platform_test_common.sh" linux') && bootstrap_macos.include?('exec "$ROOT/scripts/platform_test_common.sh" macos'), 'platform bootstrap entry points must delegate to the shared open replacement test script' ) assert!( platform_test_common.include?("cd '$ROOT/shorebird' && '$DART_BIN' pub get") && platform_test_common.include?('test/src/shorebird_env_test.dart') && platform_test_common.include?('test/src/commands/doctor_command_test.dart') && platform_test_common.include?('packages/shorebird_code_push_client/test/src/code_push_client_test.dart') && platform_test_common.include?('packages/artifact_proxy/test/artifact_proxy_test.dart') && platform_test_common.include?('packages/artifact_proxy/test/server_bin_test.dart') && platform_test_common.include?('CARGO_BIN="${CARGO_BIN:-cargo}"') && platform_test_common.include?('--manifest-path "$ROOT/updater/library/Cargo.toml"'), 'platform bootstrap must test CLI, code push client, artifact proxy, and updater open server defaults' ) assert!( step_uses(jobs.fetch('source-checks'), 'dtolnay/rust-toolchain@stable'), 'source-checks must install Rust so updater runtime tests run in default CI' ) source_bootstrap_step = jobs.fetch('source-checks').fetch('steps', []).find do |step| step['run'].to_s.include?('./scripts/bootstrap_linux.sh') end assert!( source_bootstrap_step && source_bootstrap_step.dig('env', 'SKIP_GCLIENT_SYNC') == '1', 'source-checks must run bootstrap_linux.sh with SKIP_GCLIENT_SYNC=1 for source-level CI' ) assert!( job_runs(jobs.fetch('source-checks')).join("\n").include?('python3 -m py_compile scripts/write_artifact_manifest.py'), 'source-checks must compile the artifact manifest helper' ) assert!( job_runs(jobs.fetch('source-checks')).join("\n").include?('python3 -m py_compile scripts/validate_artifact_mirror.py'), 'source-checks must compile the artifact mirror validator' ) assert!( job_runs(jobs.fetch('source-checks')).join("\n").include?('python3 -m py_compile scripts/validate_release_manifest.py'), 'source-checks must compile the release manifest validator' ) assert!( job_runs(jobs.fetch('source-checks')).join("\n").include?('python3 -m py_compile scripts/write_release_manifest.py'), 'source-checks must compile the release manifest helper' ) assert!( job_runs(jobs.fetch('source-checks')).join("\n").include?('./scripts/verify_ci_capacity.sh'), 'source-checks must smoke-test heavy runner capacity checks' ) assert!( job_runs(jobs.fetch('source-checks')).join("\n").include?('./scripts/verify_sync_open_sources.sh'), 'source-checks must smoke-test source sync behavior in an isolated workspace' ) assert!( job_runs(jobs.fetch('source-checks')).join("\n").include?('./scripts/verify_open_infrastructure_defaults.sh'), 'source-checks must verify open infrastructure defaults independently of the workflow contract' ) assert!( verify_open_infrastructure_defaults.include?('BUILD_SENSITIVE_FILES') && verify_open_infrastructure_defaults.include?('check_forbidden_in_tree "$ROOT/shorebird/packages/artifact_proxy/lib" dart') && verify_open_infrastructure_defaults.include?('https://git.tonycloud.org/dart-lang/sdk.git') && verify_open_infrastructure_defaults.include?('https://git.tonycloud.org/flutter/shorebird-updater.git') && verify_open_infrastructure_defaults.include?('http://localhost:8080/download.flutter.io') && verify_open_infrastructure_defaults.include?('http://localhost:8080/artifacts') && verify_open_infrastructure_defaults.include?('defaultHostedUrl = ') && verify_open_infrastructure_defaults.include?('download.shorebird.dev') && verify_open_infrastructure_defaults.include?('shorebird-dart-sdk-prebuilt'), 'open infrastructure defaults verifier must scan build-sensitive files and enforce open remotes/artifact hosts' ) assert!( job_runs(jobs.fetch('source-checks')).join("\n").include?('./scripts/verify_powershell_open_defaults.sh'), 'source-checks must verify Windows PowerShell launcher/updater open defaults' ) assert!( verify_powershell_open_defaults.include?('shorebird/bin/shorebird.ps1') && verify_powershell_open_defaults.include?('flutter/bin/internal/update_dart_sdk.ps1') && verify_powershell_open_defaults.include?('System.Management.Automation.Language.Parser') && verify_powershell_open_defaults.include?('https://git.tonycloud.org/flutter/flutter.git') && verify_powershell_open_defaults.include?('http://localhost:8080/download.flutter.io') && verify_powershell_open_defaults.include?('download.shorebird.dev') && verify_powershell_open_defaults.include?('github.com/shorebirdtech/flutter.git'), 'PowerShell verifier must parse Windows scripts and enforce open Flutter/artifact defaults' ) assert!( verify_sync_open_sources.include?('expected forbidden Dart SDK remote to fail') && verify_sync_open_sources.include?('expected forbidden updater source remote to fail') && verify_sync_open_sources.include?('expected forbidden explicit UPDATER_URL to fail'), 'source sync smoke test must reject upstream Dart SDK and official Shorebird updater remotes' ) assert!( job_runs(jobs.fetch('source-checks')).join("\n").include?('./scripts/verify_release_manifest.sh'), 'source-checks must smoke-test release manifest validation' ) assert!( job_runs(jobs.fetch('source-checks')).join("\n").include?('./scripts/verify_artifact_mirror_validator.sh'), 'source-checks must smoke-test standalone artifact mirror validation' ) assert!( job_runs(jobs.fetch('source-checks')).join("\n").include?('./scripts/verify_assemble_artifact_mirror.sh'), 'source-checks must smoke-test assembling downloaded CI artifacts into a publishable mirror' ) assert!( job_runs(jobs.fetch('source-checks')).join("\n").include?('./scripts/verify_artifact_mirror_workflow_assembly.sh'), 'source-checks must smoke-test the artifact-mirror aggregation job with fake downloaded artifacts' ) assert!( job_runs(jobs.fetch('source-checks')).join("\n").include?('./scripts/verify_hosted_full_sdk_build.sh --help >/dev/null 2>&1'), 'source-checks must smoke-test hosted full-SDK helper argument parsing without contacting GitHub' ) assert!( job_runs(jobs.fetch('source-checks')).join("\n").include?('./scripts/verify_ios_interpreter_route_validator.sh'), 'source-checks must smoke-test iOS interpreter route artifact validation' ) assert!( job_runs(jobs.fetch('source-checks')).join("\n").include?('bash -n scripts/*.sh shorebird/third_party/flutter/bin/internal/shared.sh flutter/bin/internal/update_dart_sdk.sh'), 'source-checks must syntax-check the patched Flutter launchers/updaters as well as repo scripts' ) assert!( job_runs(jobs.fetch('source-checks')).join("\n").include?('python3 -m py_compile scripts/safe_extract_tar.py'), 'source-checks must syntax-check the shared safe tar extractor' ) assert!( job_runs(jobs.fetch('source-checks')).join("\n").include?("compile(Path('flutter/DEPS').read_text") && job_runs(jobs.fetch('source-checks')).join("\n").include?("'flutter/DEPS', 'exec'"), 'source-checks must syntax-check Flutter DEPS before heavy gclient sync' ) assemble_artifact_mirror = read_repo_file(repo_root, 'scripts/assemble_artifact_mirror.sh') validate_artifact_mirror = read_repo_file(repo_root, 'scripts/validate_artifact_mirror.py') validate_release_manifest = read_repo_file(repo_root, 'scripts/validate_release_manifest.py') safe_extract_tar = read_repo_file(repo_root, 'scripts/safe_extract_tar.py') verify_assemble_artifact_mirror = read_repo_file( repo_root, 'scripts/verify_assemble_artifact_mirror.sh' ) verify_artifact_mirror_validator = read_repo_file( repo_root, 'scripts/verify_artifact_mirror_validator.sh' ) verify_artifact_mirror_workflow_assembly = read_repo_file( repo_root, 'scripts/verify_artifact_mirror_workflow_assembly.sh' ) verify_downloaded_release_artifacts = read_repo_file( repo_root, 'scripts/verify_downloaded_release_artifacts.sh' ) verify_hosted_full_sdk_build = read_repo_file( repo_root, 'scripts/verify_hosted_full_sdk_build.sh' ) verify_upload_readiness = read_repo_file(repo_root, 'scripts/verify_upload_readiness.sh') write_release_manifest = read_repo_file(repo_root, 'scripts/write_release_manifest.py') verify_release_manifest = read_repo_file(repo_root, 'scripts/verify_release_manifest.sh') assert!( assemble_artifact_mirror.include?('scan_for_shorebird_trees "$INPUT_DIR"') && assemble_artifact_mirror.include?("find \"$INPUT_DIR\" -type f \\( -name '*.tar.gz' -o -name '*.tgz' \\)") && assemble_artifact_mirror.include?('scripts/safe_extract_tar.py') && assemble_artifact_mirror.include?('conflicting mirror file') && assemble_artifact_mirror.include?('validate_artifact_mirror.py') && assemble_artifact_mirror.include?('write_sha256.sh'), 'artifact mirror assembler must safely extract archives, merge direct and archived shorebird/ subtrees, reject conflicts, write checksum sidecars, and run the shared mirror validator' ) assert!( safe_extract_tar.include?('unsafe archive member path') && safe_extract_tar.include?('unsupported archive member type') && safe_extract_tar.include?('duplicate archive member path') && safe_extract_tar.include?('archive member escapes extraction root') && safe_extract_tar.include?('tarfile.open') && safe_extract_tar.include?('extract_safe_tar_archive'), 'shared safe tar extractor must reject unsafe paths, unsupported members, duplicate file entries, and extraction escapes' ) assert!( validate_artifact_mirror.include?('REQUIRED_PATCH_ZIPS') && validate_artifact_mirror.include?('"patch-linux-x64.zip": "patch"') && validate_artifact_mirror.include?('"patch-darwin-x64.zip": "patch"') && validate_artifact_mirror.include?('"patch-darwin-arm64.zip": "patch"') && validate_artifact_mirror.include?('"patch-windows-x64.zip": "patch.exe"') && validate_artifact_mirror.include?('validate_sidecars') && validate_artifact_mirror.include?('validate_manifest_overrides') && validate_artifact_mirror.include?('validate_patch_zips') && validate_artifact_mirror.include?('is_safe_relative_path') && validate_artifact_mirror.include?('is_plain_file') && validate_artifact_mirror.include?('unsafe artifact override path') && validate_artifact_mirror.include?('artifact override is empty') && validate_artifact_mirror.include?('symlink entries are not allowed') && validate_artifact_mirror.include?('replace("$engine", engine_revision)') && validate_artifact_mirror.include?('zipfile.ZipFile(zip_path)') && validate_artifact_mirror.include?('names != [expected_entry]'), 'artifact mirror validator must validate sidecars, manifest overrides, and patch-tool ZIP contracts' ) assert!( verify_assemble_artifact_mirror.include?('linux-engine-x64.tar.gz') && verify_assemble_artifact_mirror.include?('patch-linux-x64.zip') && verify_assemble_artifact_mirror.include?('patch-darwin-x64.zip') && verify_assemble_artifact_mirror.include?('patch-darwin-arm64.zip') && verify_assemble_artifact_mirror.include?('patch-windows-x64.zip') && verify_assemble_artifact_mirror.include?('artifacts_manifest.yaml') && verify_assemble_artifact_mirror.include?('unexpectedly allowed a conflicting mirror file') && verify_assemble_artifact_mirror.include?('unexpectedly allowed an unsafe tar member') && verify_assemble_artifact_mirror.include?('unexpectedly allowed a tar symlink member') && verify_assemble_artifact_mirror.include?('unexpectedly allowed a duplicate tar member') && verify_assemble_artifact_mirror.include?('unexpectedly allowed an invalid patch zip') && verify_assemble_artifact_mirror.include?('unexpectedly allowed a missing manifest override'), 'artifact mirror assembler smoke test must cover archived engine subtrees, patch artifacts, metadata, conflicts, unsafe tar members, duplicate tar members, invalid patch zips, and missing overrides' ) assert!( verify_artifact_mirror_validator.include?('validate_artifact_mirror.py') && verify_artifact_mirror_validator.include?('unexpectedly accepted a stale sidecar') && verify_artifact_mirror_validator.include?('unexpectedly accepted an unsafe manifest override') && verify_artifact_mirror_validator.include?('unexpectedly accepted a symlink artifact') && verify_artifact_mirror_validator.include?('unexpectedly accepted an empty manifest override artifact') && verify_artifact_mirror_validator.include?('verify_assemble_artifact_mirror.sh'), 'artifact mirror validator smoke test must cover valid mirrors, unsafe override paths, empty override artifacts, symlink artifacts, and stale sidecar rejection' ) assert!( verify_artifact_mirror_workflow_assembly.include?('mirror-input') && verify_artifact_mirror_workflow_assembly.include?('open-shorebird-artifact-mirror.tar.gz') && verify_artifact_mirror_workflow_assembly.include?('open-shorebird-release-manifest.json') && verify_artifact_mirror_workflow_assembly.include?('write_release_manifest.py') && verify_artifact_mirror_workflow_assembly.include?('validate_release_manifest.py') && verify_artifact_mirror_workflow_assembly.include?('verify_downloaded_release_artifacts.sh') && verify_artifact_mirror_workflow_assembly.include?('validate_artifact_mirror.py') && verify_artifact_mirror_workflow_assembly.include?('safe_extract_tar.py') && verify_artifact_mirror_workflow_assembly.include?('mirror-metadata/*artifacts_manifest.yaml') && verify_artifact_mirror_workflow_assembly.include?('flutter_patched_sdk_product.zip') && verify_artifact_mirror_workflow_assembly.include?('unexpectedly accepted downloaded artifacts for the wrong github_sha') && verify_artifact_mirror_workflow_assembly.include?('unexpectedly accepted a stale downloaded release manifest sidecar') && verify_artifact_mirror_workflow_assembly.include?('unexpectedly accepted a stale downloaded mirror archive sidecar') && verify_artifact_mirror_workflow_assembly.include?('unexpectedly accepted an unsafe downloaded mirror archive'), 'artifact-mirror workflow dry-run must exercise mirror-only input assembly, release manifest requirements, downloaded sidecar rejection, and unsafe downloaded mirror archive rejection' ) assert!( verify_downloaded_release_artifacts.include?('open-shorebird-release-manifest.json') && verify_downloaded_release_artifacts.include?('open-shorebird-artifact-mirror.tar.gz') && verify_downloaded_release_artifacts.include?('validate_release_manifest.py') && verify_downloaded_release_artifacts.include?('validate_artifact_mirror.py') && verify_downloaded_release_artifacts.include?('--github-sha') && verify_downloaded_release_artifacts.include?('EXPECTED_GITHUB_SHA') && verify_downloaded_release_artifacts.include?('validate_manifest_args') && verify_downloaded_release_artifacts.include?('verify_sha256_sidecar') && verify_downloaded_release_artifacts.include?('scripts/safe_extract_tar.py') && verify_downloaded_release_artifacts.include?('manifest_sidecar') && verify_downloaded_release_artifacts.include?('mirror_sidecar') && verify_downloaded_release_artifacts.include?('digest mismatch') && verify_downloaded_release_artifacts.include?('filename mismatch') && verify_downloaded_release_artifacts.include?('expected exactly one') && verify_downloaded_release_artifacts.include?('mirror archive did not contain open-shorebird-artifact-mirror/'), 'downloaded release verifier must validate manifest and mirror checksum sidecars plus safely extracted mirror archive from downloaded GitHub artifacts' ) assert!( verify_hosted_full_sdk_build.include?('gh workflow run "$WORKFLOW"') && verify_hosted_full_sdk_build.include?('-f full_sdk_build=true') && verify_hosted_full_sdk_build.include?('-f run_runtime_smokes=false') && verify_hosted_full_sdk_build.include?('gh run download "$run_id"') && verify_hosted_full_sdk_build.include?('--json status,conclusion,url,headSha') && verify_hosted_full_sdk_build.include?('--github-sha "$run_head_sha"') && verify_hosted_full_sdk_build.include?('unable to read headSha') && verify_hosted_full_sdk_build.include?('verify_downloaded_release_artifacts.sh') && verify_hosted_full_sdk_build.include?('--linux-heavy-runner') && verify_hosted_full_sdk_build.include?('--macos-heavy-runner') && verify_hosted_full_sdk_build.include?('--skip-gclient-sync'), 'hosted full SDK verifier must dispatch the manual full build, download artifacts, and run the release artifact verifier' ) verify_ci_workflow_shell = read_repo_file(repo_root, 'scripts/verify_ci_workflow.sh') assert!( verify_ci_workflow_shell.include?('--require-tracked') && verify_ci_workflow_shell.include?('--require-clean') && verify_ci_workflow_shell.include?('--require-upload-ready') && verify_ci_workflow_shell.include?('RUBY_ARGS'), 'workflow verifier shell wrapper must expose tracked, clean, and upload-ready modes' ) assert!( verify_upload_readiness.include?('verify_ci_workflow.sh') && verify_upload_readiness.include?('--require-upload-ready') && verify_upload_readiness.include?('upload readiness check passed'), 'upload readiness verifier must run the workflow contract in upload-ready mode' ) assert!( write_release_manifest.include?('artifact_count') && write_release_manifest.include?('github_sha') && write_release_manifest.include?('"artifact_group": artifact_relative.parts[0]') && write_release_manifest.include?('"filename": artifact_path.name') && write_release_manifest.include?('fnmatch.fnmatchcase') && write_release_manifest.include?('missing required artifact matching') && write_release_manifest.include?('is_safe_relative_path') && write_release_manifest.include?('is_plain_file') && write_release_manifest.include?('unsafe artifact path') && write_release_manifest.include?('empty artifacts are not allowed') && write_release_manifest.include?('symlink artifacts are not allowed') && write_release_manifest.include?('parse_sidecar') && write_release_manifest.include?('digest mismatch') && write_release_manifest.include?('filename mismatch') && write_release_manifest.include?('orphan .sha256 sidecars'), 'release manifest helper must validate checksum sidecars and write artifact provenance' ) assert!( validate_release_manifest.include?('format_version') && validate_release_manifest.include?('artifact_count') && validate_release_manifest.include?('github_sha is') && validate_release_manifest.include?('artifact_group must be a string') && validate_release_manifest.include?('does not match path group') && validate_release_manifest.include?('filename must be a string') && validate_release_manifest.include?('does not match path filename') && validate_release_manifest.include?('is_safe_relative_path') && validate_release_manifest.include?('is_plain_file') && validate_release_manifest.include?('PurePosixPath') && validate_release_manifest.include?('unsafe artifact path') && validate_release_manifest.include?('duplicate artifact path') && validate_release_manifest.include?('missing artifact file') && validate_release_manifest.include?('empty artifacts are not allowed') && validate_release_manifest.include?('size mismatch') && validate_release_manifest.include?('digest mismatch') && validate_release_manifest.include?('sidecar digest mismatch') && validate_release_manifest.include?('artifacts missing from release manifest') && validate_release_manifest.include?('sidecars missing from release manifest'), 'release manifest validator must verify manifest structure, digests, sizes, sidecars, and complete artifact coverage' ) assert!( verify_release_manifest.include?('open-shorebird-cli-linux-x64.tar.gz') && verify_release_manifest.include?('shorebird-server-linux-amd64.tar.gz') && verify_release_manifest.include?('validate_release_manifest.py') && verify_release_manifest.include?('unexpectedly accepted a tampered digest') && verify_release_manifest.include?('unexpectedly accepted the wrong github_sha') && verify_release_manifest.include?('unexpectedly accepted an unlisted artifact') && verify_release_manifest.include?('unexpectedly accepted an unsafe artifact path') && verify_release_manifest.include?('unexpectedly accepted bad provenance fields') && verify_release_manifest.include?('unexpectedly accepted a symlink artifact') && verify_release_manifest.include?('unexpectedly accepted an empty artifact') && verify_release_manifest.include?('unexpectedly accepted a missing sidecar') && verify_release_manifest.include?('unexpectedly accepted a bad sidecar') && verify_release_manifest.include?('unexpectedly accepted an orphan sidecar') && verify_release_manifest.include?('unexpectedly accepted a missing required artifact'), 'release manifest smoke test must cover valid artifacts, required globs, provenance fields, path safety, symlink rejection, and checksum failure modes' ) assert!( sync_open_sources.include?('real_path()') && sync_open_sources.include?('ensure_source_link "$DART_TARGET" "$DART_SRC" "Dart SDK"') && sync_open_sources.include?('reject_forbidden_remotes') && sync_open_sources.include?('github.com/dart-lang/sdk') && sync_open_sources.include?('dart.googlesource.com/sdk') && sync_open_sources.include?('github.com/shorebirdtech/updater') && sync_open_sources.include?('target symlink points at') && sync_open_sources.include?('UPDATER_URL="${UPDATER_URL:-}"') && sync_open_sources.include?('ensure_source_link "$TARGET" "$UPDATER_SRC" "updater submodule"') && sync_open_sources.include?('link_checkout "$target" "$source" "$label"'), 'open source sync must force engine Dart/updater dependencies to workspace submodules and reject known unpatched/official remotes' ) assert!( !sync_open_sources.include?('github.com/shorebirdtech/updater.git'), 'open source sync must not default to the official Shorebird updater repository' ) assert!( !open_replacements_doc.include?('github.com/shorebirdtech/updater.git') && !open_replacements_doc.include?('sync_open_sources.ps1') && !open_replacements_doc.include?('public `shorebirdtech/updater`'), 'open replacement audit must document local submodules, not official upstream clone instructions' ) required_jobs = %w[ source-checks cli-artifacts server-artifacts custom-dart-sdk custom-dart-sdk-macos linux-engine android-engine web-sdk ios-engine artifact-mirror linux-runtime-smoke android-runtime-smoke ] missing_jobs = required_jobs.reject { |job_name| jobs.key?(job_name) } assert!(missing_jobs.empty?, "missing jobs: #{missing_jobs.join(', ')}") (required_jobs - ['source-checks']).each do |job_name| assert!( Array(jobs.fetch(job_name).fetch('needs', [])).include?('source-checks'), "#{job_name} must depend on source-checks before producing artifacts or running runtime smokes" ) end on_config = workflow['on'] || workflow[true] || {} inputs = on_config.dig('workflow_dispatch', 'inputs') || {} assert!( workflow.dig('defaults', 'run', 'shell') == 'bash', 'workflow must default run steps to bash for cross-platform script portability' ) required_inputs = %w[ full_sdk_build run_gclient_sync base_flutter_engine_revision linux_heavy_runner macos_heavy_runner sdk_min_free_disk_gb engine_min_free_disk_gb run_runtime_smokes linux_runtime_runner android_runtime_runner android_serial ] missing_inputs = required_inputs.reject { |input| inputs.key?(input) } assert!(missing_inputs.empty?, "missing workflow_dispatch inputs: #{missing_inputs.join(', ')}") assert!( workflow.dig('env', 'BASE_FLUTTER_ENGINE_REVISION').to_s.include?('base_flutter_engine_revision'), 'workflow must expose base_flutter_engine_revision as BASE_FLUTTER_ENGINE_REVISION' ) assert!( workflow.dig('env', 'SDK_MIN_FREE_DISK_GB').to_s.include?('sdk_min_free_disk_gb') && workflow.dig('env', 'ENGINE_MIN_FREE_DISK_GB').to_s.include?('engine_min_free_disk_gb'), 'workflow must expose dispatch-configurable heavy job disk thresholds' ) assert!( workflow.dig('env', 'DEPOT_TOOLS_UPDATE').to_s == '0' && platform_test_common.include?('DEPOT_TOOLS_UPDATE="${DEPOT_TOOLS_UPDATE:-0}"'), 'workflow and bootstrap must keep depot_tools pinned unless explicitly overridden' ) %w[source-checks cli-artifacts server-artifacts].each do |job_name| condition = jobs.fetch(job_name).fetch('if', '') assert!( !condition.to_s.include?('full_sdk_build'), "#{job_name} must run by default, not only during full_sdk_build" ) end %w[custom-dart-sdk custom-dart-sdk-macos linux-engine android-engine web-sdk ios-engine].each do |job_name| condition = jobs.fetch(job_name).fetch('if', '').to_s assert!( condition.include?('workflow_dispatch') && condition.include?('full_sdk_build'), "#{job_name} must be gated by manual full_sdk_build dispatch" ) run_text = job_runs(jobs.fetch(job_name)).join("\n") assert!( run_text.include?('python3 --version') && run_text.include?('gclient help >/dev/null') && run_text.include?('ninja --version'), "#{job_name} must verify depot_tools-provided build tools before heavy builds" ) end { 'custom-dart-sdk' => 'SDK_MIN_FREE_DISK_GB', 'custom-dart-sdk-macos' => 'SDK_MIN_FREE_DISK_GB', 'linux-engine' => 'ENGINE_MIN_FREE_DISK_GB', 'android-engine' => 'ENGINE_MIN_FREE_DISK_GB', 'web-sdk' => 'ENGINE_MIN_FREE_DISK_GB', 'ios-engine' => 'ENGINE_MIN_FREE_DISK_GB', }.each do |job_name, disk_env_var| run_text = job_runs(jobs.fetch(job_name)).join("\n") assert!( run_text.include?('scripts/check_ci_capacity.sh') && run_text.include?("CI_MIN_FREE_DISK_GB=\"$#{disk_env_var}\""), "#{job_name} must fail early on runners with insufficient free disk" ) end artifact_mirror_condition = jobs.fetch('artifact-mirror').fetch('if', '').to_s assert!( artifact_mirror_condition.include?('workflow_dispatch') && artifact_mirror_condition.include?('full_sdk_build'), 'artifact-mirror must be gated by manual full_sdk_build dispatch' ) %w[cli-artifacts linux-engine android-engine web-sdk ios-engine].each do |dependency| assert!( Array(jobs.fetch('artifact-mirror').fetch('needs', [])).include?(dependency), "artifact-mirror must depend on #{dependency}" ) end %w[server-artifacts custom-dart-sdk custom-dart-sdk-macos].each do |dependency| assert!( Array(jobs.fetch('artifact-mirror').fetch('needs', [])).include?(dependency), "artifact-mirror must depend on #{dependency} so release manifest covers it" ) end %w[linux-runtime-smoke android-runtime-smoke].each do |job_name| condition = jobs.fetch(job_name).fetch('if', '').to_s assert!( condition.include?('workflow_dispatch') && condition.include?('run_runtime_smokes'), "#{job_name} must be gated by manual run_runtime_smokes dispatch" ) assert!( !condition.include?('full_sdk_build'), "#{job_name} must not require full_sdk_build" ) end cli_matrix = matrix_include(jobs.fetch('cli-artifacts')) expected_cli_targets = [ ['ubuntu-latest', 'linux', 'x64', 'patch-linux-x64.zip', 'patch'], ['macos-15-intel', 'macos', 'x64', 'patch-darwin-x64.zip', 'patch'], ['macos-14', 'macos', 'arm64', 'patch-darwin-arm64.zip', 'patch'], ['windows-latest', 'windows', 'x64', 'patch-windows-x64.zip', 'patch.exe'], ] expected_cli_targets.each do |runner_os, artifact_os, artifact_arch, patch_zip, patch_entry| assert!( cli_matrix.any? do |entry| entry['os'] == runner_os && entry['artifact_os'] == artifact_os && entry['artifact_arch'] == artifact_arch && entry['patch_zip'] == patch_zip && entry['patch_entry'] == patch_entry end, "missing CLI target #{artifact_os}/#{artifact_arch} on #{runner_os} with #{patch_zip}" ) end assert!( upload_names(jobs.fetch('cli-artifacts')).include?('cli-${{ matrix.artifact_os }}-${{ matrix.artifact_arch }}'), 'CLI artifacts must be uploaded' ) assert!( upload_names(jobs.fetch('cli-artifacts')).include?('mirror-${{ matrix.patch_zip }}'), 'mirror patch artifacts must be uploaded' ) assert!( upload_names(jobs.fetch('cli-artifacts')).include?('mirror-metadata'), 'mirror artifact metadata must be uploaded' ) assert!( step_uses(jobs.fetch('cli-artifacts'), 'dtolnay/rust-toolchain@stable'), 'CLI artifact job must install a stable Rust toolchain' ) assert!( job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('cargo build --release --manifest-path updater/patch/Cargo.toml --bin patch'), 'CLI artifact job must build the public updater patch binary' ) assert!( job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('names = archive.namelist()') && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('names != [expected_entry]') && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('info.file_size <= 0') && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('${{ matrix.patch_entry }}'), 'CLI artifact job must verify patch mirror ZIPs contain the exact patch entry expected by the cache' ) assert!( job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('mirror_patch_dir="artifacts/mirror/shorebird/$engine_revision"') && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('cp "artifacts/mirror/${{ matrix.patch_zip }}" "$mirror_patch_dir/${{ matrix.patch_zip }}"') && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('test -f "$mirror_patch_dir/${{ matrix.patch_zip }}"') && jobs.fetch('cli-artifacts').fetch('steps').any? do |step| step.dig('with', 'path').to_s.include?('artifacts/mirror/shorebird/**/${{ matrix.patch_zip }}') && step.dig('with', 'path').to_s.include?('artifacts/mirror/shorebird/**/${{ matrix.patch_zip }}.sha256') end, 'CLI artifact job must upload a publish-ready shorebird// patch mirror subtree' ) assert!( job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('scripts/write_artifact_manifest.py') && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('artifacts_manifest.yaml') && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('artifacts/mirror/shorebird/$engine_revision') && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('base_engine_revision="${BASE_FLUTTER_ENGINE_REVISION:-$engine_revision}"') && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('--flutter-engine-revision "$base_engine_revision"'), 'CLI artifact job must generate open artifact mirror metadata' ) assert!( job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('packages/artifact_proxy/bin/server.dart') && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('bin/artifact_proxy${{ matrix.extension }}') && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('artifact_proxy${{ matrix.extension }} --health-check'), 'CLI artifact job must compile, package, and smoke-test the open artifact proxy binary' ) assert!( job_runs(jobs.fetch('cli-artifacts')).join("\n").include?("grep -q 'https://git.tonycloud.org/flutter/shorebird.git' <<<\"$version_output\"") && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?("grep -q 'https://git.tonycloud.org/flutter/shorebird.git' <<<\"$extracted_version_output\"") && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?("git@github.com:shorebirdtech/shorebird.git") && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('compiled CLI still reports the official Shorebird SSH remote') && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('extracted CLI still reports the official Shorebird SSH remote'), 'CLI artifact job must assert compiled and extracted version banners report the open fork' ) assert!( job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('cli_extract_dir="$(mktemp -d)"') && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('tar -C "$cli_extract_dir" -xzf open-shorebird-cli-${{ matrix.artifact_os }}-${{ matrix.artifact_arch }}.tar.gz') && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('test -f "$cli_extract_dir/manifest.json"') && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('"$cli_extract_dir/bin/open_aot_patch_tools${{ matrix.extension }}" --help') && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('"$cli_extract_dir/bin/shorebird${{ matrix.extension }}" --version') && job_runs(jobs.fetch('cli-artifacts')).join("\n").include?('"$cli_extract_dir/bin/artifact_proxy${{ matrix.extension }}" --health-check'), 'CLI artifact job must extract and smoke-test the downloadable archive layout' ) server_matrix = matrix_include(jobs.fetch('server-artifacts')) expected_server_targets = [ ['linux', 'amd64'], ['linux', 'arm64'], ['darwin', 'amd64'], ['darwin', 'arm64'], ['windows', 'amd64'], ] expected_server_targets.each do |goos, goarch| assert!( server_matrix.any? { |entry| entry['goos'] == goos && entry['goarch'] == goarch }, "missing server target #{goos}/#{goarch}" ) end assert!( upload_names(jobs.fetch('server-artifacts')).include?('shorebird-server-${{ matrix.goos }}-${{ matrix.goarch }}'), 'server artifacts must be uploaded' ) assert!( inputs.dig('linux_heavy_runner', 'default') == 'open-shorebird-linux-heavy' && inputs.dig('macos_heavy_runner', 'default') == 'open-shorebird-macos-heavy', 'heavy SDK/engine workflow inputs must default to custom large-runner labels' ) assert!( jobs.fetch('custom-dart-sdk').fetch('runs-on').to_s.include?("inputs.linux_heavy_runner || 'open-shorebird-linux-heavy'") && jobs.fetch('android-engine').fetch('runs-on').to_s.include?("inputs.linux_heavy_runner || 'open-shorebird-linux-heavy'") && jobs.fetch('web-sdk').fetch('runs-on').to_s.include?("inputs.linux_heavy_runner || 'open-shorebird-linux-heavy'") && jobs.fetch('custom-dart-sdk-macos').fetch('runs-on').to_s.include?("inputs.macos_heavy_runner || 'open-shorebird-macos-heavy'") && jobs.fetch('ios-engine').fetch('runs-on').to_s.include?("inputs.macos_heavy_runner || 'open-shorebird-macos-heavy'"), 'heavy SDK/engine jobs must default to custom large-runner labels, not standard hosted runners' ) assert!( workflow.dig('env', 'JAVA_VERSION').to_s == '17', 'workflow must pin the Java version used by Android builds' ) assert!( free_ci_disk_linux.include?('GITHUB_ACTIONS') && free_ci_disk_linux.include?('RUNNER_ENVIRONMENT:-github-hosted') && free_ci_disk_linux.include?('CI_FREE_DISK_SPACE_FORCE') && free_ci_disk_linux.include?('CI_FREE_DISK_SPACE:-1') && free_ci_disk_linux.include?('sudo rm -rf "$path"'), 'Linux disk cleanup must be explicitly gated for GitHub-hosted runners and overridable on custom runners' ) assert!( check_ci_capacity.include?('CI_MIN_FREE_DISK_GB') && check_ci_capacity.include?('CI_CAPACITY_PATH') && check_ci_capacity.include?('CI_AVAILABLE_DISK_KB_OVERRIDE') && check_ci_capacity.include?('df -Pk "$CHECK_PATH"') && check_ci_capacity.include?('runner has insufficient free disk'), 'heavy runner capacity check must validate free disk with portable df output and a test override' ) assert!( verify_ci_capacity.include?('CI_AVAILABLE_DISK_KB_OVERRIDE') && verify_ci_capacity.include?('unexpectedly accepted insufficient disk') && verify_ci_capacity.include?('unexpectedly accepted an invalid minimum') && verify_ci_capacity.include?('CI_MIN_FREE_DISK_GB=0'), 'capacity check smoke test must cover pass, fail, invalid input, and disabled modes' ) assert!( step_uses(jobs.fetch('android-engine'), 'actions/setup-java@v4') && workflow_text.include?('distribution: temurin') && workflow_text.include?('java-version: ${{ env.JAVA_VERSION }}') && job_runs(jobs.fetch('android-runtime-smoke')).join("\n").include?('java -version') && android_runtime_smoke.include?('require_tool java'), 'Android engine and runtime smoke paths must provision or verify Java explicitly' ) server_artifact_run_text = job_runs(jobs.fetch('server-artifacts')).join("\n") assert!( server_artifact_run_text.include?('GOOS" == "linux"') && server_artifact_run_text.include?('GOARCH" == "amd64"') && server_artifact_run_text.include?('cp internal/api/handlers/openapi.yaml') && server_artifact_run_text.include?('"openapi.yaml"') && server_artifact_run_text.include?('curl -fsS http://127.0.0.1:18080/health') && server_artifact_run_text.include?('grep -q \'"status":"ok"\'') && server_artifact_run_text.include?('grep -q \'\'') && server_artifact_run_text.include?('curl -fsS http://127.0.0.1:18080/openapi.yaml') && server_artifact_run_text.include?("grep -q 'openapi: 3.1.0'"), 'server artifact job must package and smoke-test the native Linux package from its archive' ) required_uploads = { 'custom-dart-sdk' => ['custom-dart-sdk-linux-x64'], 'custom-dart-sdk-macos' => ['custom-dart-sdk-macos-arm64'], 'linux-engine' => ['linux-engine-x64'], 'android-engine' => ['android-engine-arm64'], 'web-sdk' => ['flutter-web-sdk'], 'ios-engine' => ['ios-interpreter-engine', 'macos-engine-arm64'], 'artifact-mirror' => ['open-shorebird-artifact-mirror', 'open-shorebird-release-manifest'], } required_uploads.each do |job_name, expected_names| actual_names = upload_names(jobs.fetch(job_name)) expected_names.each do |expected_name| assert!(actual_names.include?(expected_name), "#{job_name} must upload #{expected_name}") end end jobs.each do |job_name, job| job.fetch('steps', []).each do |step| next unless step['uses'].to_s.start_with?('actions/upload-artifact@') assert!( step.dig('with', 'if-no-files-found') == 'error', "#{job_name} upload #{step.dig('with', 'name')} must fail on missing files" ) assert!( step.dig('with', 'path').to_s.include?('.sha256'), "#{job_name} upload #{step.dig('with', 'name')} must include a checksum sidecar" ) end end run_text_by_job = jobs.transform_values { |job| job_runs(job).join("\n") } assert!( run_text_by_job.fetch('cli-artifacts').include?('"flutter_revision": "${flutter_revision}"') && run_text_by_job.fetch('cli-artifacts').include?('"engine_revision": "${engine_revision}"'), 'CLI artifact manifest must record Flutter and engine revisions' ) assert!( run_text_by_job.fetch('server-artifacts').include?('server_revision="$(git rev-parse HEAD)"') && run_text_by_job.fetch('server-artifacts').include?('"server_git_sha": "${server_revision}"'), 'server artifact manifest must record the server source revision' ) %w[custom-dart-sdk custom-dart-sdk-macos].each do |job_name| assert!( run_text_by_job.fetch(job_name).include?('dart_sdk_revision="$(git -C dart-sdk-new rev-parse HEAD)"') && run_text_by_job.fetch(job_name).include?('"dart_sdk_git_sha": "${dart_sdk_revision}"'), "#{job_name} manifest must record the Dart SDK source revision" ) end { 'custom-dart-sdk' => 'custom-dart-sdk-linux-x64', 'custom-dart-sdk-macos' => 'custom-dart-sdk-macos-arm64', }.each do |job_name, artifact_name| run_text = run_text_by_job.fetch(job_name) assert!( run_text.include?('sdk_extract_dir="$(mktemp -d)"') && run_text.include?("tar -C \"$sdk_extract_dir\" -xzf #{artifact_name}.tar.gz") && run_text.include?("test -f \"$sdk_extract_dir/#{artifact_name}/manifest.json\"") && run_text.include?("test -f \"$sdk_extract_dir/#{artifact_name}/args.gn\"") && run_text.include?("test -x \"$sdk_extract_dir/#{artifact_name}/dart-sdk/bin/dart\"") && run_text.include?("test -x \"$sdk_extract_dir/#{artifact_name}/gen_snapshot\"") && run_text.include?("test -x \"$sdk_extract_dir/#{artifact_name}/dartaotruntime\"") && run_text.include?("\"$sdk_extract_dir/#{artifact_name}/dart-sdk/bin/dart\" --version"), "#{job_name} must extract and smoke-test the downloadable Dart SDK archive layout" ) end %w[linux-engine android-engine web-sdk ios-engine].each do |job_name| assert!( run_text_by_job.fetch(job_name).include?('engine_revision="$(cat flutter/bin/internal/engine.version)"') && run_text_by_job.fetch(job_name).include?('"engine_revision": "${engine_revision}"'), "#{job_name} manifest must record the Flutter engine revision" ) end engine_archive_checks = { 'linux-engine' => [ 'engine_extract_dir="$(mktemp -d)"', 'tar -C "$engine_extract_dir" -xzf linux-engine-x64.tar.gz', 'test -f "$engine_extract_dir/linux-engine/manifest.json"', 'test -f "$engine_extract_dir/linux-engine/linux_release_x64.args.gn"', 'test -f "$engine_extract_dir/linux-engine/linux-x64-flutter-gtk.zip"', 'test -f "$engine_extract_dir/linux-engine/flutter_patched_sdk_product.zip"', 'test -f "$engine_extract_dir/linux-engine/artifacts.zip"', 'test -f "$engine_extract_dir/linux-engine/mirror/shorebird/flutter_infra_release/flutter/$engine_revision/linux-x64-release/linux-x64-flutter-gtk.zip"', 'test -f "$engine_extract_dir/linux-engine/mirror/shorebird/flutter_infra_release/flutter/$engine_revision/linux-x64-release/artifacts.zip"', 'test -f "$engine_extract_dir/linux-engine/mirror/shorebird/flutter_infra_release/flutter/$engine_revision/flutter_patched_sdk_product.zip"', ], 'android-engine' => [ 'engine_extract_dir="$(mktemp -d)"', 'tar -C "$engine_extract_dir" -xzf android-engine-arm64.tar.gz', 'test -f "$engine_extract_dir/android-engine/manifest.json"', 'test -f "$engine_extract_dir/android-engine/android_release_arm64.args.gn"', 'test -f "$engine_extract_dir/android-engine/artifacts.zip"', 'test -f "$engine_extract_dir/android-engine/symbols.zip"', 'test -f "$engine_extract_dir/android-engine/flutter.jar"', 'test -f "$engine_extract_dir/android-engine/libflutter.so"', 'test -f "$engine_extract_dir/android-engine/gen_snapshot_arm64"', 'test -f "$engine_extract_dir/android-engine/analyze_snapshot_arm64"', 'test -f "$engine_extract_dir/android-engine/mirror/shorebird/flutter_infra_release/flutter/$engine_revision/android-arm64-release/artifacts.zip"', 'test -f "$engine_extract_dir/android-engine/mirror/shorebird/flutter_infra_release/flutter/$engine_revision/android-arm64-release/symbols.zip"', ], 'web-sdk' => [ 'sdk_extract_dir="$(mktemp -d)"', 'tar -C "$sdk_extract_dir" -xzf flutter-web-sdk.tar.gz', 'test -f "$sdk_extract_dir/web-sdk/manifest.json"', 'test -f "$sdk_extract_dir/web-sdk/wasm_release.args.gn"', 'test -f "$sdk_extract_dir/web-sdk/flutter-web-sdk.zip"', 'test -f "$sdk_extract_dir/web-sdk/mirror/shorebird/flutter_infra_release/flutter/$engine_revision/flutter-web-sdk.zip"', ], 'ios-engine' => [ 'engine_extract_dir="$(mktemp -d)"', 'tar -C "$engine_extract_dir" -xzf ios-interpreter-engine.tar.gz', 'test -f "$engine_extract_dir/ios-engine/manifest.json"', 'test -d "$engine_extract_dir/ios-engine/Flutter.framework"', 'test -d "$engine_extract_dir/ios-engine/Flutter.xcframework"', 'test -f "$engine_extract_dir/ios-engine/ios-release/artifacts.zip"', 'test -f "$engine_extract_dir/ios-engine/ios_release.args.gn"', 'test -f "$engine_extract_dir/ios-engine/host_release_arm64.args.gn"', 'test -x "$engine_extract_dir/ios-engine/host_release_arm64/gen_snapshot"', 'test -f "$engine_extract_dir/ios-engine/mirror/shorebird/flutter_infra_release/flutter/$engine_revision/ios-release/artifacts.zip"', '"Flutter.xcframework/Info.plist"', '"Flutter.xcframework/ios-arm64/Flutter.framework/Flutter"', '"gen_snapshot_arm64"', '"analyze_snapshot_arm64"', '"entitlements.txt"', '"without_entitlements.txt"', '"unsigned_binaries.txt"', 'tar -C "$engine_extract_dir" -xzf macos-engine-arm64.tar.gz', 'test -f "$engine_extract_dir/macos-engine/manifest.json"', 'test -f "$engine_extract_dir/macos-engine/macos_release_arm64.args.gn"', 'test -f "$engine_extract_dir/macos-engine/FlutterMacOS.framework.zip"', 'test -f "$engine_extract_dir/macos-engine/flutter_patched_sdk_product.zip"', 'test -f "$engine_extract_dir/macos-engine/mirror/shorebird/flutter_infra_release/flutter/$engine_revision/darwin-arm64-release/FlutterMacOS.framework.zip"', ], } engine_archive_checks.each do |job_name, expected_texts| run_text = run_text_by_job.fetch(job_name) missing_texts = expected_texts.reject { |expected_text| run_text.include?(expected_text) } assert!( missing_texts.empty?, "#{job_name} must extract and smoke-test downloadable engine archive layout: #{missing_texts.join(', ')}" ) end expected_outputs = { 'cli-artifacts' => [ 'open-shorebird-cli-${{ matrix.artifact_os }}-${{ matrix.artifact_arch }}.tar.gz', 'artifacts/mirror/${{ matrix.patch_zip }}', 'artifacts_manifest.yaml', ], 'server-artifacts' => [ 'shorebird-server-${GOOS}-${GOARCH}.tar.gz', ], 'custom-dart-sdk' => [ 'custom-dart-sdk-linux-x64.tar.gz', ], 'custom-dart-sdk-macos' => [ 'custom-dart-sdk-macos-arm64.tar.gz', ], 'linux-engine' => [ 'linux-engine-x64.tar.gz', ], 'android-engine' => [ 'android-engine-arm64.tar.gz', ], 'web-sdk' => [ 'flutter-web-sdk.tar.gz', ], 'ios-engine' => [ 'ios-interpreter-engine.tar.gz', 'macos-engine-arm64.tar.gz', ], 'artifact-mirror' => [ 'open-shorebird-artifact-mirror.tar.gz', 'open-shorebird-release-manifest.json', ], } expected_outputs.each do |job_name, outputs| run_text = run_text_by_job.fetch(job_name) outputs.each do |output| assert!(run_text.include?(output), "#{job_name} must create #{output}") assert!( run_text.include?("#{output}.sha256"), "#{job_name} must create checksum sidecar for #{output}" ) end end %w[ cli-artifacts server-artifacts custom-dart-sdk custom-dart-sdk-macos linux-engine android-engine web-sdk ios-engine artifact-mirror ].each do |job_name| assert!( run_text_by_job.fetch(job_name).include?('write_sha256.sh'), "#{job_name} must use scripts/write_sha256.sh for checksum sidecars" ) end assert!( run_text_by_job.fetch('custom-dart-sdk').include?('verify_dart_sdk_args.sh dart-sdk-new/out/ReleaseX64/args.gn'), 'Linux Dart SDK job must verify patched SDK args' ) assert!( run_text_by_job.fetch('custom-dart-sdk-macos').include?('verify_dart_sdk_args.sh dart-sdk-new/xcodebuild/ReleaseARM64/args.gn'), 'macOS Dart SDK job must verify patched SDK args' ) %w[custom-dart-sdk custom-dart-sdk-macos].each do |job_name| run_text = run_text_by_job.fetch(job_name) assert!(run_text.include?('dart_dynamic_modules=false'), "#{job_name} must disable DDM") assert!(run_text.include?('dart_enable_aot_patching=true'), "#{job_name} must enable AOT patching") assert!(run_text.include?('dart_enable_shorebird_interpreter=true'), "#{job_name} must enable the interpreter route") assert!(run_text.include?('DartAPI_AotPatchingConfiguration'), "#{job_name} must run AOT patching VM tests") assert!(run_text.include?('DartAPI_BytecodePatchReloadConfiguration'), "#{job_name} must run bytecode reload VM tests") end assert!( run_text_by_job.fetch('ios-engine').include?('--shorebird-interpreter') && run_text_by_job.fetch('ios-engine').include?("--gn-args='dart_dynamic_modules=false dart_enable_aot_patching=true dart_enable_shorebird_interpreter=true shorebird_use_interpreter=true'") && run_text_by_job.fetch('ios-engine').include?('verify_ios_interpreter_route.sh') && run_text_by_job.fetch('ios-engine').include?('test -x flutter/engine/src/out/host_release_arm64/gen_snapshot') && run_text_by_job.fetch('ios-engine').include?('host_release_arm64/gen_snapshot') && run_text_by_job.fetch('ios-engine').include?('ios-release/artifacts.zip') && run_text_by_job.fetch('ios-engine').include?('mirror/shorebird/flutter_infra_release/flutter/${engine_revision}/ios-release/artifacts.zip') && run_text_by_job.fetch('ios-engine').include?('"dart_enable_aot_patching": true') && run_text_by_job.fetch('ios-engine').include?('"dart_enable_shorebird_interpreter": true') && run_text_by_job.fetch('ios-engine').include?('"shorebird_enable_aot_patching": false') && run_text_by_job.fetch('ios-engine').include?('"shorebird_use_interpreter": true'), 'Apple engine job must build, verify, package, and describe the iOS interpreter host snapshotter' ) assert!( verify_ios_interpreter_route.include?('json.load(file)') && verify_ios_interpreter_route.include?('metadata = artifact.get("metadata")') && verify_ios_interpreter_route.scan('require_gn_value "$args_file" dart_enable_aot_patching true').length >= 2 && verify_ios_interpreter_route.include?('sub("[[:space:]]*$", "", value)') && verify_ios_interpreter_route.include?('require(metadata, "runtime_mode", "dart-bytecode-interpreter", "metadata")') && verify_ios_interpreter_route.include?('require(metadata, "target_os", "ios", "metadata")') && verify_ios_interpreter_route.include?('require(metadata, "target_arch", "arm64", "metadata")') && verify_ios_interpreter_route.include?('require(artifact, "payload_kind", "full-snapshot", "artifact")') && verify_ios_interpreter_route.include?('require(encryption, "algorithm", "AES-256-GCM", "encryption")') && verify_ios_interpreter_route.include?('require_base64(artifact, "encrypted_payload_base64", "artifact")') && verify_ios_interpreter_route.include?('payload_sha256') && verify_ios_interpreter_route.include?('aad_sha256'), 'iOS route gate must parse and validate encrypted interpreter artifact metadata, not grep raw strings' ) assert!( verify_ios_interpreter_route_validator.include?('write_artifact "$valid_artifact" "dart-bytecode-interpreter"') && verify_ios_interpreter_route_validator.scan('dart_enable_aot_patching = true').length >= 2 && verify_ios_interpreter_route_validator.include?('dart_enable_aot_patching = false') && verify_ios_interpreter_route_validator.include?('shorebird_use_interpreter = false') && verify_ios_interpreter_route_validator.include?('write_artifact "$bad_runtime" "dart-dynamic-modules"') && verify_ios_interpreter_route_validator.include?('write_artifact "$bad_target" "dart-bytecode-interpreter" "android"') && verify_ios_interpreter_route_validator.include?('unexpectedly accepted malformed JSON') && verify_ios_interpreter_route_validator.include?('unexpectedly accepted a Mach-O patch artifact'), 'iOS route validator smoke test must cover valid interpreter artifacts and invalid native/DDM/malformed cases' ) assert!( [verify_engine_args, verify_dart_sdk_args, verify_ios_interpreter_route].all? do |script| script.include?('read_gn_value()') && script.include?('sub("[[:space:]]*$", "", value)') && script.include?('print value') end, 'GN arg verifiers must read the final assignment so default and explicit args match GN semantics' ) assert!( run_text_by_job.fetch('linux-engine').include?('verify_engine_args.sh') && run_text_by_job.fetch('linux-engine').include?('flutter/engine/src/out/linux_release_x64/args.gn') && run_text_by_job.fetch('linux-engine').include?('dart_enable_aot_patching=true') && run_text_by_job.fetch('linux-engine').include?('dart_enable_shorebird_interpreter=false') && run_text_by_job.fetch('linux-engine').include?('shorebird_enable_aot_patching=true') && run_text_by_job.fetch('linux-engine').include?('shorebird_use_interpreter=false') && run_text_by_job.fetch('linux-engine').include?('linux-x64-flutter-gtk.zip') && run_text_by_job.fetch('linux-engine').include?('flutter_patched_sdk_product.zip') && run_text_by_job.fetch('linux-engine').include?('mirror/shorebird/flutter_infra_release/flutter/${engine_revision}/linux-x64-release/artifacts.zip') && run_text_by_job.fetch('linux-engine').include?('mirror/shorebird/flutter_infra_release/flutter/${engine_revision}/flutter_patched_sdk_product.zip'), 'Linux engine job must build and verify the native AOT patch runtime and package GTK/patched SDK artifacts' ) assert!( run_text_by_job.fetch('android-engine').include?('verify_engine_args.sh') && run_text_by_job.fetch('android-engine').include?('flutter/engine/src/out/android_release_arm64/args.gn') && run_text_by_job.fetch('android-engine').include?('dart_enable_aot_patching=true') && run_text_by_job.fetch('android-engine').include?('dart_enable_shorebird_interpreter=false') && run_text_by_job.fetch('android-engine').include?('shorebird_enable_aot_patching=true') && run_text_by_job.fetch('android-engine').include?('shorebird_use_interpreter=false') && run_text_by_job.fetch('android-engine').include?('mirror/shorebird/flutter_infra_release/flutter/${engine_revision}/android-arm64-release/artifacts.zip') && run_text_by_job.fetch('android-engine').include?('mirror/shorebird/flutter_infra_release/flutter/${engine_revision}/android-arm64-release/symbols.zip'), 'Android engine job must build and verify the native AOT patch runtime without DDM or interpreter mode' ) assert!( run_text_by_job.fetch('web-sdk').include?('verify_engine_args.sh') && run_text_by_job.fetch('web-sdk').include?('flutter/engine/src/out/wasm_release/args.gn') && run_text_by_job.fetch('web-sdk').include?('dart_dynamic_modules=false') && run_text_by_job.fetch('web-sdk').include?('mirror/shorebird/flutter_infra_release/flutter/${engine_revision}/flutter-web-sdk.zip'), 'web SDK job must explicitly disable and verify DDM' ) assert!( run_text_by_job.fetch('ios-engine').include?('verify_engine_args.sh') && run_text_by_job.fetch('ios-engine').include?('flutter/engine/src/out/macos_release_arm64/args.gn') && run_text_by_job.fetch('ios-engine').include?('dart_enable_aot_patching=true') && run_text_by_job.fetch('ios-engine').include?('dart_enable_shorebird_interpreter=false') && run_text_by_job.fetch('ios-engine').include?('shorebird_enable_aot_patching=true') && run_text_by_job.fetch('ios-engine').include?('shorebird_use_interpreter=false') && run_text_by_job.fetch('ios-engine').include?('FlutterMacOS.framework.zip') && run_text_by_job.fetch('ios-engine').include?('flutter_patched_sdk_product.zip') && run_text_by_job.fetch('ios-engine').include?('mirror/shorebird/flutter_infra_release/flutter/${engine_revision}/darwin-arm64-release/FlutterMacOS.framework.zip'), 'macOS engine artifact must build and verify the native AOT patch runtime without DDM or interpreter mode, while leaving the shared patched SDK mirror path to linux-engine' ) assert!( run_text_by_job.fetch('linux-runtime-smoke').include?('test -d flutter/engine/src/out/linux_release_x64') && run_text_by_job.fetch('linux-runtime-smoke').include?('scripts/linux_runtime_patch_smoke.sh'), 'Linux runtime smoke job must require linux_release_x64 and run scripts/linux_runtime_patch_smoke.sh' ) assert!( run_text_by_job.fetch('android-runtime-smoke').include?('scripts/android_runtime_patch_smoke.sh'), 'Android runtime smoke job must run scripts/android_runtime_patch_smoke.sh' ) artifact_mirror_run_text = run_text_by_job.fetch('artifact-mirror') assert!( jobs.fetch('artifact-mirror').fetch('steps').count do |step| step['uses'].to_s == 'actions/download-artifact@v4' end >= 6, 'artifact-mirror must download mirror patch/metadata and engine artifact producers' ) %w[ mirror-* cli-* shorebird-server-* custom-dart-sdk-linux-x64 custom-dart-sdk-macos-arm64 linux-engine-x64 android-engine-arm64 flutter-web-sdk ios-interpreter-engine macos-engine-arm64 ].each do |download_name| assert!( jobs.fetch('artifact-mirror').fetch('steps').any? do |step| step['uses'].to_s == 'actions/download-artifact@v4' && step.fetch('with', {}).values.any? { |value| value.to_s.include?(download_name) } end, "artifact-mirror must download #{download_name}" ) end assert!( artifact_mirror_run_text.include?('./scripts/assemble_artifact_mirror.sh') && artifact_mirror_run_text.include?('downloaded-artifacts') && artifact_mirror_run_text.include?('mkdir -p mirror-input') && artifact_mirror_run_text.include?('cp -R downloaded-artifacts/mirror-* mirror-input/') && artifact_mirror_run_text.include?('cp -R downloaded-artifacts/linux-engine-x64 mirror-input/') && artifact_mirror_run_text.include?('cp -R downloaded-artifacts/android-engine-arm64 mirror-input/') && artifact_mirror_run_text.include?('cp -R downloaded-artifacts/flutter-web-sdk mirror-input/') && artifact_mirror_run_text.include?('cp -R downloaded-artifacts/ios-interpreter-engine mirror-input/') && artifact_mirror_run_text.include?('cp -R downloaded-artifacts/macos-engine-arm64 mirror-input/') && artifact_mirror_run_text.match?( %r{\./scripts/assemble_artifact_mirror\.sh\s+\\\n\s+mirror-input\s+\\\n\s+artifacts/open-shorebird-artifact-mirror} ) && artifact_mirror_run_text.include?('artifacts/open-shorebird-artifact-mirror') && artifact_mirror_run_text.include?('tar -C artifacts -czf open-shorebird-artifact-mirror.tar.gz open-shorebird-artifact-mirror') && artifact_mirror_run_text.include?('python3 scripts/safe_extract_tar.py open-shorebird-artifact-mirror.tar.gz "$mirror_extract_dir"') && artifact_mirror_run_text.include?('open-shorebird-artifact-mirror.tar.gz.sha256') && artifact_mirror_run_text.include?('validate_artifact_mirror.py "$mirror_extract_dir/open-shorebird-artifact-mirror"') && artifact_mirror_run_text.include?('scripts/write_release_manifest.py') && artifact_mirror_run_text.include?('scripts/validate_release_manifest.py') && artifact_mirror_run_text.include?('"$manifest_input"') && artifact_mirror_run_text.include?('open-shorebird-release-manifest.json') && artifact_mirror_run_text.include?('open-shorebird-release-manifest.json.sha256') && artifact_mirror_run_text.include?('--github-sha "${{ github.sha }}"'), 'artifact-mirror must assemble, archive, checksum, and manifest a publishable mirror root' ) %w[ cli-linux-x64/*open-shorebird-cli-linux-x64.tar.gz cli-macos-x64/*open-shorebird-cli-macos-x64.tar.gz cli-macos-arm64/*open-shorebird-cli-macos-arm64.tar.gz cli-windows-x64/*open-shorebird-cli-windows-x64.tar.gz shorebird-server-linux-amd64/*shorebird-server-linux-amd64.tar.gz shorebird-server-linux-arm64/*shorebird-server-linux-arm64.tar.gz shorebird-server-darwin-amd64/*shorebird-server-darwin-amd64.tar.gz shorebird-server-darwin-arm64/*shorebird-server-darwin-arm64.tar.gz shorebird-server-windows-amd64/*shorebird-server-windows-amd64.tar.gz custom-dart-sdk-linux-x64/*custom-dart-sdk-linux-x64.tar.gz custom-dart-sdk-macos-arm64/*custom-dart-sdk-macos-arm64.tar.gz linux-engine-x64/*linux-engine-x64.tar.gz android-engine-arm64/*android-engine-arm64.tar.gz flutter-web-sdk/*flutter-web-sdk.tar.gz ios-interpreter-engine/*ios-interpreter-engine.tar.gz macos-engine-arm64/*macos-engine-arm64.tar.gz mirror-patch-linux-x64.zip/*patch-linux-x64.zip mirror-patch-darwin-x64.zip/*patch-darwin-x64.zip mirror-patch-darwin-arm64.zip/*patch-darwin-arm64.zip mirror-patch-windows-x64.zip/*patch-windows-x64.zip mirror-metadata/*artifacts_manifest.yaml open-shorebird-artifact-mirror/*open-shorebird-artifact-mirror.tar.gz ].each do |required_glob| assert!( artifact_mirror_run_text.include?("--require '#{required_glob}'"), "artifact-mirror release manifest must require #{required_glob}" ) end %w[ artifacts_manifest.yaml patch-linux-x64.zip patch-darwin-x64.zip patch-darwin-arm64.zip patch-windows-x64.zip android-arm64-release/artifacts.zip android-arm64-release/symbols.zip linux-x64-release/artifacts.zip linux-x64-release/linux-x64-flutter-gtk.zip ios-release/artifacts.zip flutter-web-sdk.zip darwin-arm64-release/FlutterMacOS.framework.zip flutter_patched_sdk_product.zip ].each do |required_path| assert!( artifact_mirror_run_text.include?(required_path), "artifact-mirror archive smoke must verify #{required_path}" ) end assert!( !workflow_text.match?(/\bdart_dynamic_modules\s*=\s*true\b/), 'workflow must not enable DART_DYNAMIC_MODULES' ) assert!( !workflow_text.include?('aot-tools.dill'), 'workflow must not publish the legacy closed/native AOT tools artifact' ) assert!( !workflow_text.include?('shasum'), 'workflow must use scripts/write_sha256.sh instead of runner-specific shasum' ) assert!( workflow_text.include?('scripts/write_sha256.sh'), 'workflow must use the workspace checksum helper' ) puts "workflow yaml ok: #{workflow_path}" puts "workflow run blocks ok: #{scripts.length} checked" puts "workflow artifact contract ok"