snapshot of https://github.com/flutter/samples.git (main, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Main Branch CI / Test Flutter beta on ubuntu-latest (push) Has been skipped
Main Branch CI / Test Flutter stable on ubuntu-latest (push) Has been skipped
Main Branch CI / Test Flutter beta on macos-latest (push) Has been cancelled
Main Branch CI / Test Flutter stable on macos-latest (push) Has been cancelled
Some checks failed
Main Branch CI / Test Flutter beta on ubuntu-latest (push) Has been skipped
Main Branch CI / Test Flutter stable on ubuntu-latest (push) Has been skipped
Main Branch CI / Test Flutter beta on macos-latest (push) Has been cancelled
Main Branch CI / Test Flutter stable on macos-latest (push) Has been cancelled
This commit is contained in:
88
tool/android_ci_script.sh
Executable file
88
tool/android_ci_script.sh
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
flutter doctor -v
|
||||
|
||||
echo "Fetching dependencies and building 'prebuilt_module/flutter_module/'."
|
||||
pushd add_to_app/prebuilt_module/flutter_module/
|
||||
flutter packages get
|
||||
flutter build aar
|
||||
popd
|
||||
|
||||
echo "Fetching dependencies for 'plugin/flutter_module_using_plugin'."
|
||||
pushd add_to_app/plugin/flutter_module_using_plugin
|
||||
flutter packages get
|
||||
popd
|
||||
|
||||
echo "Fetching dependencies for 'fullscreen/'."
|
||||
pushd add_to_app/fullscreen/flutter_module
|
||||
flutter packages get
|
||||
popd
|
||||
|
||||
echo "Fetching dependencies for 'multiple_flutters/'."
|
||||
pushd add_to_app/multiple_flutters/multiple_flutters_module
|
||||
flutter packages get
|
||||
popd
|
||||
|
||||
declare -ar ANDROID_PROJECT_NAMES=(
|
||||
"add_to_app/fullscreen/android_fullscreen" \
|
||||
"add_to_app/plugin/android_using_plugin" \
|
||||
"add_to_app/multiple_flutters/multiple_flutters_android" \
|
||||
"add_to_app/prebuilt_module/android_using_prebuilt_module" \
|
||||
)
|
||||
|
||||
for PROJECT_NAME in "${ANDROID_PROJECT_NAMES[@]}"
|
||||
do
|
||||
echo "== Testing '${PROJECT_NAME}' on Flutter's stable channel =="
|
||||
pushd "${PROJECT_NAME}"
|
||||
|
||||
./gradlew --stacktrace assembleDebug
|
||||
./gradlew --stacktrace assembleRelease
|
||||
|
||||
popd
|
||||
done
|
||||
|
||||
# If the credentials don't exist, this script isn't being run from within the
|
||||
# flutter/samples repo. Rather than throw an error, allow the test to pass
|
||||
# successfully.
|
||||
if [ ! -f "svc-keyfile.json" ]
|
||||
then
|
||||
echo "Keyfile for Firebase Test Lab not found. Skipping integration tests."
|
||||
echo "-- Success --"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# At this time, espresso tests only exist for android_fullscreen. These will
|
||||
# eventually be rolled out to each Android project and included in the loop
|
||||
# above.
|
||||
echo "== Espresso testing 'android_fullscreen' on Flutter's ${FLUTTER_VERSION} channel =="
|
||||
pushd "add_to_app/fullscreen/android_fullscreen"
|
||||
./gradlew app:assembleAndroidTest
|
||||
./gradlew app:assembleDebug -Ptarget=../flutter_module/test_driver/example.dart
|
||||
gcloud auth activate-service-account --key-file=../../svc-keyfile.json
|
||||
gcloud --quiet config set project test-lab-project-ccbec
|
||||
gcloud firebase test android run --type instrumentation \
|
||||
--app app/build/outputs/apk/debug/app-debug.apk \
|
||||
--test app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk\
|
||||
--timeout 5m
|
||||
popd
|
||||
|
||||
echo "== Run e2e test for testing_app =="
|
||||
pushd "testing_app"
|
||||
readonly APP_DIR=$(pwd)
|
||||
flutter packages get
|
||||
flutter build apk
|
||||
pushd "android"
|
||||
./gradlew app:assembleAndroidTest
|
||||
./gradlew app:assembleRelease -Ptarget=${APP_DIR}/test/perf_test_e2e.dart
|
||||
popd
|
||||
gcloud auth activate-service-account --key-file=../svc-keyfile.json
|
||||
gcloud --quiet config set project test-lab-project-ccbec
|
||||
gcloud firebase test android run --type instrumentation \
|
||||
--app build/app/outputs/apk/release/app-release.apk \
|
||||
--test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk \
|
||||
--timeout 5m
|
||||
popd
|
||||
|
||||
echo "-- Success --"
|
||||
119
tool/ci_script.dart
Normal file
119
tool/ci_script.dart
Normal file
@@ -0,0 +1,119 @@
|
||||
// ignore_for_file: avoid_print,
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
final rootDir = Directory.current;
|
||||
final pubspecFile = File(path.join(rootDir.path, 'pubspec.yaml'));
|
||||
final pubspecContent = await pubspecFile.readAsString();
|
||||
final pubspecYaml = loadYaml(pubspecContent);
|
||||
|
||||
// contains full list of samples
|
||||
final workspace = pubspecYaml['workspace'] as YamlList?;
|
||||
|
||||
if (workspace == null) {
|
||||
print('No workspace found in pubspec.yaml');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// CI always skips the samples on this list
|
||||
final skipCiList = pubspecYaml['skip_ci'] as YamlList?;
|
||||
|
||||
// CI will also skip for samples on specific branches
|
||||
// This is useful when API changes in the beta branch are causing CI to fail
|
||||
// and there isn't a solution that works in both beta and main
|
||||
final channel = await _getFlutterChannel();
|
||||
print('Current Flutter channel: $channel');
|
||||
|
||||
final channelSkipKey = 'skip_ci_$channel';
|
||||
final channelSkipList = pubspecYaml[channelSkipKey] as YamlList?;
|
||||
if (channelSkipList != null) {
|
||||
print('Applying skips for channel "$channel": $channelSkipList');
|
||||
}
|
||||
|
||||
// pub workspace, only run 'get' once
|
||||
await _runCommand('flutter', ['pub', 'get'], workingDirectory: rootDir.path);
|
||||
|
||||
final packages = workspace
|
||||
.where((e) {
|
||||
if (skipCiList != null && skipCiList.contains(e)) return false;
|
||||
if (channelSkipList != null && channelSkipList.contains(e)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((e) => e.toString())
|
||||
.toList();
|
||||
|
||||
for (final package in packages) {
|
||||
final packagePath = path.join(rootDir.path, package);
|
||||
print('== Testing \'$package\' ==');
|
||||
await _runCommand('dart', [
|
||||
'analyze',
|
||||
'--fatal-infos',
|
||||
'--fatal-warnings',
|
||||
], workingDirectory: packagePath);
|
||||
|
||||
await _runCommand('dart', ['format', '.'], workingDirectory: packagePath);
|
||||
|
||||
if (await Directory(path.join(packagePath, 'test')).exists()) {
|
||||
final packagePubspecFile = File(path.join(packagePath, 'pubspec.yaml'));
|
||||
final packagePubspecContent = await packagePubspecFile.readAsString();
|
||||
if (packagePubspecContent.contains('flutter:')) {
|
||||
await _runCommand('flutter', [
|
||||
'test',
|
||||
'--no-pub',
|
||||
], workingDirectory: packagePath);
|
||||
} else {
|
||||
await _runCommand('dart', ['test'], workingDirectory: packagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _runCommand(
|
||||
String executable,
|
||||
List<String> arguments, {
|
||||
String? workingDirectory,
|
||||
}) async {
|
||||
final process = await Process.start(
|
||||
executable,
|
||||
arguments,
|
||||
workingDirectory: workingDirectory,
|
||||
runInShell: true,
|
||||
mode: ProcessStartMode.inheritStdio,
|
||||
);
|
||||
final exitCode = await process.exitCode;
|
||||
if (exitCode != 0) {
|
||||
print(
|
||||
'Command "$executable ${arguments.join(' ')}" failed with exit code $exitCode in $workingDirectory',
|
||||
);
|
||||
exit(exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _getFlutterChannel() async {
|
||||
try {
|
||||
final result = await Process.run('flutter', [
|
||||
'--version',
|
||||
'--machine',
|
||||
], runInShell: true);
|
||||
if (result.exitCode != 0) {
|
||||
print(
|
||||
'Flutter version command failed with exit code ${result.exitCode}',
|
||||
);
|
||||
print('Stdout: ${result.stdout}');
|
||||
print('Stderr: ${result.stderr}');
|
||||
return 'unknown';
|
||||
}
|
||||
final machineInfo = jsonDecode(result.stdout as String);
|
||||
return machineInfo['channel'] as String? ?? 'unknown';
|
||||
} catch (e) {
|
||||
print('Error detecting Flutter channel: $e');
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
140
tool/ci_with_channel.dart
Normal file
140
tool/ci_with_channel.dart
Normal file
@@ -0,0 +1,140 @@
|
||||
// ignore_for_file: avoid_print,
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
/// Do not use this with GitHub Actions. It is a convenience script for running
|
||||
/// CI locally.
|
||||
///
|
||||
/// Usage: dart tool/ci_with_channel.dart -c stable,beta
|
||||
|
||||
Future<void> main(List<String> args) async {
|
||||
final parser = ArgParser()
|
||||
..addFlag(
|
||||
'help',
|
||||
abbr: 'h',
|
||||
negatable: false,
|
||||
help: 'Print this usage information.',
|
||||
)
|
||||
..addOption(
|
||||
'channels',
|
||||
abbr: 'c',
|
||||
help:
|
||||
'Comma-separated list of channels to run on (e.g., stable,beta,main)',
|
||||
);
|
||||
|
||||
final results = parser.parse(args);
|
||||
|
||||
if (results['help'] as bool) {
|
||||
print(parser.usage);
|
||||
return;
|
||||
}
|
||||
|
||||
final channelsArg = results['channels'] as String?;
|
||||
|
||||
List<String> channels;
|
||||
if (channelsArg != null) {
|
||||
channels = channelsArg.split(',').map((c) => c.trim()).toList();
|
||||
} else {
|
||||
// Default to current channel if none specified
|
||||
channels = [];
|
||||
}
|
||||
|
||||
final rootDir = Directory.current;
|
||||
final originalChannel = await _getCurrentChannel();
|
||||
|
||||
try {
|
||||
if (channels.isEmpty) {
|
||||
print('Running on current channel: $originalChannel');
|
||||
await _runCI(rootDir);
|
||||
} else {
|
||||
for (final channel in channels) {
|
||||
print('\n=== Switching to channel: $channel ===');
|
||||
await _runCommand('flutter', ['channel', channel]);
|
||||
await _runCommand('flutter', ['doctor']);
|
||||
await _runCI(rootDir);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (channels.isNotEmpty && originalChannel != null) {
|
||||
print('\n=== Switching back to original channel: $originalChannel ===');
|
||||
await _runCommand('flutter', ['channel', originalChannel]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _getCurrentChannel() async {
|
||||
final result = await Process.run('flutter', ['channel']);
|
||||
if (result.exitCode != 0) {
|
||||
return null;
|
||||
}
|
||||
final output = result.stdout as String;
|
||||
final match = RegExp(r'\* (\w+)').firstMatch(output);
|
||||
return match?.group(1);
|
||||
}
|
||||
|
||||
Future<void> _runCI(Directory rootDir) async {
|
||||
final pubspecFile = File(path.join(rootDir.path, 'pubspec.yaml'));
|
||||
final pubspecContent = await pubspecFile.readAsString();
|
||||
final pubspecYaml = loadYaml(pubspecContent);
|
||||
|
||||
final workspace = pubspecYaml['workspace'] as YamlList?;
|
||||
if (workspace == null) {
|
||||
print('No workspace found in pubspec.yaml');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// pub workspace, only run 'get' once
|
||||
await _runCommand('flutter', ['pub', 'get'], workingDirectory: rootDir.path);
|
||||
|
||||
final packages = workspace.map((e) => e.toString()).toList();
|
||||
|
||||
for (final package in packages) {
|
||||
final packagePath = path.join(rootDir.path, package);
|
||||
print('== Testing \'$package\' ==');
|
||||
await _runCommand('dart', [
|
||||
'analyze',
|
||||
'--fatal-infos',
|
||||
'--fatal-warnings',
|
||||
], workingDirectory: packagePath);
|
||||
|
||||
await _runCommand('dart', ['format', '.'], workingDirectory: packagePath);
|
||||
|
||||
if (await Directory(path.join(packagePath, 'test')).exists()) {
|
||||
final packagePubspecFile = File(path.join(packagePath, 'pubspec.yaml'));
|
||||
final packagePubspecContent = await packagePubspecFile.readAsString();
|
||||
if (packagePubspecContent.contains('flutter:')) {
|
||||
await _runCommand('flutter', [
|
||||
'test',
|
||||
'--no-pub',
|
||||
], workingDirectory: packagePath);
|
||||
} else {
|
||||
await _runCommand('dart', ['test'], workingDirectory: packagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _runCommand(
|
||||
String executable,
|
||||
List<String> arguments, {
|
||||
String? workingDirectory,
|
||||
}) async {
|
||||
final process = await Process.start(
|
||||
executable,
|
||||
arguments,
|
||||
workingDirectory: workingDirectory,
|
||||
runInShell: true,
|
||||
mode: ProcessStartMode.inheritStdio,
|
||||
);
|
||||
final exitCode = await process.exitCode;
|
||||
if (exitCode != 0) {
|
||||
print(
|
||||
'Command "$executable ${arguments.join(' ')}" failed with exit code $exitCode in $workingDirectory',
|
||||
);
|
||||
exit(exitCode);
|
||||
}
|
||||
}
|
||||
79
tool/clean_all.dart
Normal file
79
tool/clean_all.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
// ignore_for_file: avoid_print,
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
/// Runs `flutter clean` in every package listed in the root pubspec.yaml
|
||||
/// workspace. Useful for clearing stale build artifacts after switching Flutter
|
||||
/// channels.
|
||||
Future<void> main() async {
|
||||
final rootDir = Directory.current;
|
||||
final pubspecFile = File(path.join(rootDir.path, 'pubspec.yaml'));
|
||||
final pubspecContent = await pubspecFile.readAsString();
|
||||
final pubspecYaml = loadYaml(pubspecContent);
|
||||
|
||||
final workspace = pubspecYaml['workspace'] as YamlList?;
|
||||
if (workspace == null) {
|
||||
print('No workspace found in pubspec.yaml');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
final flutterBin = _resolveFlutterBin();
|
||||
print('Using flutter: $flutterBin');
|
||||
|
||||
var cleaned = 0;
|
||||
var skipped = 0;
|
||||
|
||||
for (final entry in workspace) {
|
||||
final package = entry.toString();
|
||||
final packagePath = path.join(rootDir.path, package);
|
||||
|
||||
// Only clean packages that have a pubspec.yaml (i.e. are Flutter/Dart pkgs)
|
||||
final pubspec = File(path.join(packagePath, 'pubspec.yaml'));
|
||||
if (!await pubspec.exists()) {
|
||||
print('-- Skipping \'$package\' (no pubspec.yaml found)');
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
print('== Cleaning \'$package\' ==');
|
||||
final process = await Process.start(
|
||||
flutterBin,
|
||||
['clean'],
|
||||
workingDirectory: packagePath,
|
||||
runInShell: true,
|
||||
mode: ProcessStartMode.inheritStdio,
|
||||
);
|
||||
final exitCode = await process.exitCode;
|
||||
if (exitCode != 0) {
|
||||
print(
|
||||
'Warning: flutter clean failed with exit code $exitCode in $packagePath',
|
||||
);
|
||||
} else {
|
||||
cleaned++;
|
||||
}
|
||||
}
|
||||
|
||||
print('');
|
||||
print('Done. Cleaned $cleaned package(s), skipped $skipped.');
|
||||
}
|
||||
|
||||
/// Resolves the path to the `flutter` binary by finding it relative to the
|
||||
/// running Dart SDK. Falls back to 'flutter' (uses PATH) if not found.
|
||||
String _resolveFlutterBin() {
|
||||
// Platform.resolvedExecutable points to the dart binary, e.g.:
|
||||
// /path/to/flutter/bin/cache/dart-sdk/bin/dart
|
||||
// Flutter binary is at /path/to/flutter/bin/flutter
|
||||
final dartBin = File(Platform.resolvedExecutable);
|
||||
final flutterBin = path.join(
|
||||
dartBin.parent.parent.parent.parent.path,
|
||||
'bin',
|
||||
'flutter',
|
||||
);
|
||||
if (File(flutterBin).existsSync()) {
|
||||
return flutterBin;
|
||||
}
|
||||
return 'flutter'; // fallback to PATH
|
||||
}
|
||||
85
tool/ios_ci_script.sh
Executable file
85
tool/ios_ci_script.sh
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
flutter doctor -v
|
||||
|
||||
###############################################################################
|
||||
# Helper functions
|
||||
###############################################################################
|
||||
|
||||
# Runs `packages get` on given flutter module.
|
||||
# $1 = path to flutter module
|
||||
function flutter_packages_get() {
|
||||
echo "Fetching dependencies and building '$1'."
|
||||
pushd $1
|
||||
flutter packages get
|
||||
popd
|
||||
}
|
||||
|
||||
# Runs xcode build for Debug and Release.
|
||||
# $1 = path to directory containing the xcode project
|
||||
# $2 = name of the xcworkspace
|
||||
# $3 = name of the scheme
|
||||
function build() {
|
||||
echo "== Testing '$1' on Flutter's $FLUTTER_VERSION channel =="
|
||||
pushd "$1"
|
||||
|
||||
pod install
|
||||
|
||||
xcodebuild -workspace "$2" \
|
||||
-scheme "$3" CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGN_IDENTITY=- EXPANDED_CODE_SIGN_IDENTITY=- \
|
||||
COMPILER_INDEX_STORE_ENABLE=NO CONFIGURATION=Debug | xcpretty
|
||||
|
||||
xcodebuild -workspace "$2" \
|
||||
-scheme "$3" CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGN_IDENTITY=- EXPANDED_CODE_SIGN_IDENTITY=- \
|
||||
COMPILER_INDEX_STORE_ENABLE=NO CONFIGURATION=Release \
|
||||
-destination generic/platform=iOS | xcpretty
|
||||
|
||||
popd
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Building preconditions
|
||||
###############################################################################
|
||||
|
||||
echo "Pre-caching ios artifacts, such as the Flutter.framework"
|
||||
flutter precache --no-web --no-linux --no-windows --no-fuchsia --no-android --no-macos
|
||||
|
||||
echo "Fetching dependencies and building 'prebuilt_module/flutter_module'."
|
||||
pushd add_to_app/prebuilt_module/flutter_module
|
||||
flutter packages get
|
||||
flutter build ios-framework --xcframework --output="$(pwd)/../ios_using_prebuilt_module/Flutter"
|
||||
popd
|
||||
|
||||
flutter_packages_get "add_to_app/books/flutter_module_books"
|
||||
flutter_packages_get "add_to_app/fullscreen/flutter_module"
|
||||
flutter_packages_get "add_to_app/multiple_flutters/multiple_flutters_module"
|
||||
flutter_packages_get "add_to_app/plugin/flutter_module_using_plugin"
|
||||
|
||||
###############################################################################
|
||||
# Build projects
|
||||
###############################################################################
|
||||
|
||||
build "add_to_app/books/ios_books" "IosBooks.xcworkspace" "IosBooks"
|
||||
build "add_to_app/fullscreen/ios_fullscreen" "IOSFullScreen.xcworkspace" "IOSFullScreen"
|
||||
build "add_to_app/multiple_flutters/multiple_flutters_ios" "MultipleFluttersIos.xcworkspace" "MultipleFluttersIos"
|
||||
build "add_to_app/plugin/ios_using_plugin" "IOSUsingPlugin.xcworkspace" "IOSUsingPlugin"
|
||||
|
||||
echo "== Testing 'add_to_app/prebuilt_module/ios_using_prebuilt_module' on Flutter's $FLUTTER_VERSION channel =="
|
||||
pushd "add_to_app/prebuilt_module/ios_using_prebuilt_module"
|
||||
|
||||
xcodebuild CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGN_IDENTITY=- EXPANDED_CODE_SIGN_IDENTITY=- \
|
||||
COMPILER_INDEX_STORE_ENABLE=NO CONFIGURATION=Debug | xcpretty
|
||||
|
||||
xcodebuild CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGN_IDENTITY=- EXPANDED_CODE_SIGN_IDENTITY=- \
|
||||
COMPILER_INDEX_STORE_ENABLE=NO CONFIGURATION=Release \
|
||||
-destination generic/platform=iOS | xcpretty
|
||||
|
||||
popd
|
||||
|
||||
echo "-- Success --"
|
||||
13
tool/pubspec.yaml
Normal file
13
tool/pubspec.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
name: repo_tool
|
||||
description: A tool to automate release cleanup for the samples monorepo.
|
||||
resolution: workspace
|
||||
|
||||
environment:
|
||||
sdk: ^3.9.0-0
|
||||
|
||||
dependencies:
|
||||
args: ^2.4.2
|
||||
io: ^1.0.4
|
||||
path: ^1.9.0
|
||||
yaml: ^3.1.2
|
||||
yaml_edit: ^2.1.1
|
||||
371
tool/release.dart
Normal file
371
tool/release.dart
Normal file
@@ -0,0 +1,371 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:io/ansi.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:yaml/yaml.dart';
|
||||
import 'package:yaml_edit/yaml_edit.dart';
|
||||
|
||||
const String logDir = 'logs';
|
||||
|
||||
void main(List<String> arguments) async {
|
||||
final runner = ReleaseScriptRunner();
|
||||
await runner.run(arguments);
|
||||
}
|
||||
|
||||
class ReleaseScriptRunner {
|
||||
late final File _logFile;
|
||||
bool _isDryRun = false;
|
||||
|
||||
Future<void> run(List<String> arguments) async {
|
||||
final parser = ArgParser()
|
||||
..addFlag(
|
||||
'dry-run',
|
||||
negatable: false,
|
||||
help:
|
||||
'Prints the commands that would be executed, but does not run them.',
|
||||
);
|
||||
final argResults = parser.parse(arguments);
|
||||
_isDryRun = argResults['dry-run'] as bool;
|
||||
|
||||
_setupLogging();
|
||||
|
||||
log(
|
||||
styleBold.wrap(
|
||||
'Flutter Monorepo Update Script Started: ${DateTime.now()}',
|
||||
),
|
||||
stdout,
|
||||
);
|
||||
log(blue.wrap('Log file: ${_logFile.path}')!, stdout);
|
||||
|
||||
if (_isDryRun) {
|
||||
log(yellow.wrap('--- DRY RUN MODE ---')!, stdout);
|
||||
}
|
||||
|
||||
if (!await _isFlutterInstalled()) {
|
||||
log(red.wrap('Flutter is not installed or not in PATH')!, stderr);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
final dartVersion = await _getDartVersion();
|
||||
if (dartVersion == null) {
|
||||
log(red.wrap('Failed to get Dart SDK version.'), stderr);
|
||||
exit(1);
|
||||
}
|
||||
log(blue.wrap('Using Dart SDK version: $dartVersion'), stdout);
|
||||
|
||||
final packages = await _getWorkspacePackages();
|
||||
if (packages.isEmpty) {
|
||||
log(
|
||||
yellow.wrap('No packages found in the root pubspec.yaml workspace.'),
|
||||
stdout,
|
||||
);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
log(
|
||||
blue.wrap(
|
||||
'Found ${packages.length} Flutter project(s): ${packages.join(', ')}',
|
||||
),
|
||||
stdout,
|
||||
);
|
||||
|
||||
final failedProjects = <String>[];
|
||||
for (final packagePath in packages) {
|
||||
final success = await _processProject(packagePath, dartVersion);
|
||||
if (!success) {
|
||||
failedProjects.add(packagePath);
|
||||
}
|
||||
}
|
||||
|
||||
_printSummary(packages.length, failedProjects);
|
||||
|
||||
log(
|
||||
styleBold.wrap(
|
||||
'\nFlutter Monorepo Update Script Completed: ${DateTime.now()}',
|
||||
),
|
||||
stdout,
|
||||
);
|
||||
|
||||
if (failedProjects.isNotEmpty) {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void _setupLogging() {
|
||||
final logsDir = Directory(logDir);
|
||||
if (!logsDir.existsSync()) {
|
||||
logsDir.createSync(recursive: true);
|
||||
}
|
||||
final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-');
|
||||
_logFile = File(p.join(logDir, 'release_logs_$timestamp.log'));
|
||||
}
|
||||
|
||||
void log(String? message, IOSink sink) {
|
||||
if (message == null) return;
|
||||
sink.writeln(message);
|
||||
}
|
||||
|
||||
void logToFile(String? message) {
|
||||
if (message == null) return;
|
||||
_logFile.writeAsStringSync(
|
||||
'\n${_stripAnsiCodes(message)}',
|
||||
mode: FileMode.append,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _isFlutterInstalled() async {
|
||||
try {
|
||||
final result = await Process.run('flutter', ['--version']);
|
||||
return result.exitCode == 0;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _getDartVersion() async {
|
||||
try {
|
||||
final result = await Process.run('flutter', ['--version', '--machine']);
|
||||
if (result.exitCode == 0) {
|
||||
final json = loadYaml(result.stdout as String) as YamlMap;
|
||||
final fullDartVersion = json['dartSdkVersion'] as String?;
|
||||
if (fullDartVersion != null) {
|
||||
return fullDartVersion.split(' ').first;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
log(red.wrap('Error getting Dart SDK version: $e'), stderr);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<String>> _getWorkspacePackages() async {
|
||||
final rootPubspec = File('pubspec.yaml');
|
||||
if (!rootPubspec.existsSync()) {
|
||||
log(red.wrap('Root pubspec.yaml not found!'), stderr);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
final content = await rootPubspec.readAsString();
|
||||
final yaml = loadYaml(content) as YamlMap;
|
||||
final workspace = yaml['workspace'] as YamlList?;
|
||||
if (workspace != null) {
|
||||
return workspace.nodes.map((node) => node.value as String).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
log(red.wrap('Error reading workspace packages: $e'), stderr);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<bool> _processProject(String projectPath, String dartVersion) async {
|
||||
final projectName = p.basename(projectPath);
|
||||
final projectDir = Directory(projectPath);
|
||||
final issues = <String>[];
|
||||
|
||||
if (!projectDir.existsSync()) {
|
||||
log(red.wrap('Project directory not found: $projectPath'), stderr);
|
||||
return false;
|
||||
}
|
||||
|
||||
log(styleBold.wrap('\n========================================='), stdout);
|
||||
log(styleBold.wrap('Processing project: $projectName'), stdout);
|
||||
log(styleBold.wrap('=========================================')!, stdout);
|
||||
|
||||
log(
|
||||
blue.wrap('Updating SDK constraints to use Dart $dartVersion'),
|
||||
stdout,
|
||||
);
|
||||
if (!_isDryRun) {
|
||||
if (!await _updateSdkConstraints(projectPath, dartVersion)) {
|
||||
log(
|
||||
red.wrap('Failed to update SDK constraints for $projectName'),
|
||||
stderr,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final commands = [
|
||||
Command('dart analyze', 'Running dart analyze...', 'dart', [
|
||||
'analyze',
|
||||
'--fatal-infos',
|
||||
'--fatal-warnings',
|
||||
]),
|
||||
Command('dart format', 'Running dart format...', 'dart', [
|
||||
'format',
|
||||
'.',
|
||||
]),
|
||||
];
|
||||
|
||||
final testDir = Directory(p.join(projectPath, 'test'));
|
||||
if (projectName != 'material_3_demo' && testDir.existsSync()) {
|
||||
commands.add(
|
||||
Command('flutter test', 'Running tests...', 'flutter', ['test']),
|
||||
);
|
||||
}
|
||||
|
||||
for (final command in commands) {
|
||||
log(blue.wrap(command.description), stdout);
|
||||
|
||||
final (didPass, output) = await _runCommand(
|
||||
command.executable,
|
||||
command.arguments,
|
||||
workingDirectory: projectPath,
|
||||
);
|
||||
|
||||
if (!didPass) {
|
||||
log(
|
||||
red.wrap('${command.displayName} failed for $projectName'),
|
||||
stderr,
|
||||
);
|
||||
|
||||
if (command.displayName == 'pub upgrade' ||
|
||||
command.displayName == 'pub get' &&
|
||||
output.contains('Failed to update packages.')) {
|
||||
issues.add(output);
|
||||
}
|
||||
|
||||
if (command.displayName == 'dart analyze' &&
|
||||
output.contains('issue found.') ||
|
||||
output.contains('issues found.')) {
|
||||
issues.add(output);
|
||||
}
|
||||
|
||||
if (command.displayName == 'flutter test' &&
|
||||
!output.contains('All tests passed!')) {
|
||||
issues.add(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (issues.isNotEmpty) {
|
||||
logToFile('- Issues found in $projectName');
|
||||
for (final issue in issues) {
|
||||
if (_isOnlyWhitespace(issue)) continue;
|
||||
logToFile('-- $issue');
|
||||
}
|
||||
}
|
||||
|
||||
log(green.wrap('Successfully processed $projectName'), stdout);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> _updateSdkConstraints(
|
||||
String projectDir,
|
||||
String versionString,
|
||||
) async {
|
||||
final pubspecFile = File(p.join(projectDir, 'pubspec.yaml'));
|
||||
if (!pubspecFile.existsSync()) {
|
||||
log(red.wrap('pubspec.yaml not found in $projectDir'), stderr);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final newConstraint = '^$versionString-0';
|
||||
|
||||
final content = await pubspecFile.readAsString();
|
||||
final editor = YamlEditor(content);
|
||||
editor.update(['environment', 'sdk'], newConstraint);
|
||||
|
||||
await pubspecFile.writeAsString(editor.toString());
|
||||
log(
|
||||
blue.wrap(
|
||||
'Updated Dart SDK constraint in $projectDir to: $newConstraint',
|
||||
),
|
||||
stdout,
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
log(
|
||||
red.wrap('Failed to update SDK constraint in $projectDir: $e'),
|
||||
stderr,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<(bool, String)> _runCommand(
|
||||
String executable,
|
||||
List<String> arguments, {
|
||||
String? workingDirectory,
|
||||
}) async {
|
||||
final commandString = '$executable ${arguments.join(' ')}';
|
||||
if (_isDryRun) {
|
||||
log(
|
||||
yellow.wrap(
|
||||
' [DRY RUN] Would execute: `$commandString` in `${workingDirectory ?? '.'}`',
|
||||
),
|
||||
stdout,
|
||||
);
|
||||
return (true, '');
|
||||
}
|
||||
|
||||
final process = await Process.start(
|
||||
executable,
|
||||
arguments,
|
||||
workingDirectory: workingDirectory,
|
||||
runInShell: true,
|
||||
);
|
||||
|
||||
StringBuffer output = StringBuffer('');
|
||||
|
||||
final stdoutFuture = process.stdout
|
||||
.transform(SystemEncoding().decoder)
|
||||
.forEach((line) {
|
||||
log(line, stdout);
|
||||
if (!_isOnlyWhitespace(line)) {
|
||||
output.writeln(line.trim().padLeft(2));
|
||||
}
|
||||
});
|
||||
|
||||
final stderrFuture = process.stderr
|
||||
.transform(SystemEncoding().decoder)
|
||||
.forEach((line) {
|
||||
log(red.wrap(line), stderr);
|
||||
if (!_isOnlyWhitespace(line)) {
|
||||
output.writeln(line.trim().padLeft(2));
|
||||
}
|
||||
});
|
||||
|
||||
await Future.wait([stdoutFuture, stderrFuture]);
|
||||
|
||||
return (await process.exitCode == 0, output.toString());
|
||||
}
|
||||
|
||||
void _printSummary(int total, List<String> failed) {
|
||||
log(
|
||||
styleBold.wrap('\n=========================================')!,
|
||||
stdout,
|
||||
);
|
||||
log(styleBold.wrap('Update Summary')!, stdout);
|
||||
log(styleBold.wrap('=========================================')!, stdout);
|
||||
log(blue.wrap('Total projects processed: $total'), stdout);
|
||||
log(green.wrap('Successful: ${total - failed.length}'), stdout);
|
||||
|
||||
if (failed.isNotEmpty) {
|
||||
log(red.wrap('Failed: ${failed.length}'), stderr);
|
||||
log(red.wrap('Failed projects: ${failed.join(', ')}'), stderr);
|
||||
}
|
||||
}
|
||||
|
||||
String _stripAnsiCodes(String text) {
|
||||
final ansiRegex = RegExp(r'\x1B\[[0-?]*[ -/]*[@-~]');
|
||||
return text.replaceAll(ansiRegex, '');
|
||||
}
|
||||
|
||||
bool _isOnlyWhitespace(String text) {
|
||||
return text.trim().isEmpty;
|
||||
}
|
||||
}
|
||||
|
||||
class Command {
|
||||
final String displayName;
|
||||
final String description;
|
||||
final String executable;
|
||||
final List<String> arguments;
|
||||
|
||||
Command(this.displayName, this.description, this.executable, this.arguments);
|
||||
}
|
||||
Reference in New Issue
Block a user