forked from eva/focus-flow
ci: add workspace validation and release scripts
This commit is contained in:
parent
60ca067236
commit
8b2ab6b64e
11 changed files with 513 additions and 0 deletions
56
.github/workflows/ci.yml
vendored
Normal file
56
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Runs analyzer, tests, coverage, docs, and tagged desktop packaging.
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dart-lang/setup-dart@v1
|
||||||
|
with:
|
||||||
|
sdk: stable
|
||||||
|
- run: dart pub get
|
||||||
|
- run: dart analyze
|
||||||
|
- run: dart run scripts/test.dart
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: coverage-${{ matrix.os }}
|
||||||
|
path: |
|
||||||
|
coverage/
|
||||||
|
doc/api/
|
||||||
|
|
||||||
|
package:
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: linux
|
||||||
|
- os: windows-latest
|
||||||
|
platform: windows
|
||||||
|
- os: macos-latest
|
||||||
|
platform: macos
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
- run: dart pub get
|
||||||
|
- run: dart run scripts/build.dart --platform ${{ matrix.platform }} --output build/releases
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release-${{ matrix.platform }}
|
||||||
|
path: build/releases/
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,6 +4,7 @@
|
||||||
pubspec.lock
|
pubspec.lock
|
||||||
build/
|
build/
|
||||||
coverage/
|
coverage/
|
||||||
|
doc/api/
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
|
|
|
||||||
16
scripts/bootstrap_dev.sh
Executable file
16
scripts/bootstrap_dev.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Prepare local dependencies and the default scheduler SQLite location.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
scheduler_dir="${SCHEDULER_HOME:-"$HOME/ADHD_Scheduler"}"
|
||||||
|
|
||||||
|
cd "$root_dir"
|
||||||
|
dart pub get
|
||||||
|
|
||||||
|
mkdir -p "$scheduler_dir/backups"
|
||||||
|
if [[ ! -e "$scheduler_dir/scheduler.sqlite" ]]; then
|
||||||
|
: > "$scheduler_dir/scheduler.sqlite"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'Scheduler SQLite path: %s\n' "$scheduler_dir/scheduler.sqlite"
|
||||||
147
scripts/build.dart
Normal file
147
scripts/build.dart
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
/// Builds platform-specific Flutter desktop release artifacts.
|
||||||
|
Future<void> main(List<String> arguments) async {
|
||||||
|
final platform = _option(arguments, '--platform') ?? 'current';
|
||||||
|
final outputDirectory =
|
||||||
|
Directory(_option(arguments, '--output') ?? 'build/releases');
|
||||||
|
await outputDirectory.create(recursive: true);
|
||||||
|
|
||||||
|
final resolved = platform == 'current' ? _currentPlatform() : platform;
|
||||||
|
switch (resolved) {
|
||||||
|
case 'macos':
|
||||||
|
await _buildMacOs(outputDirectory);
|
||||||
|
break;
|
||||||
|
case 'windows':
|
||||||
|
await _buildWindows(outputDirectory);
|
||||||
|
break;
|
||||||
|
case 'linux':
|
||||||
|
await _buildLinux(outputDirectory);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw ArgumentError.value(
|
||||||
|
platform,
|
||||||
|
'--platform',
|
||||||
|
'Expected current, macos, windows, or linux.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _buildMacOs(Directory outputDirectory) async {
|
||||||
|
await _run('flutter', <String>['build', 'macos', '--release']);
|
||||||
|
final releaseDir = Directory('build/macos/Build/Products/Release');
|
||||||
|
final apps = releaseDir
|
||||||
|
.listSync()
|
||||||
|
.whereType<Directory>()
|
||||||
|
.where((entry) => entry.path.endsWith('.app'))
|
||||||
|
.toList(growable: false);
|
||||||
|
if (apps.isEmpty) {
|
||||||
|
throw StateError('No macOS .app found in ${releaseDir.path}.');
|
||||||
|
}
|
||||||
|
for (final app in apps) {
|
||||||
|
await _copyDirectory(
|
||||||
|
app, Directory(_join(outputDirectory.path, _basename(app.path))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _buildWindows(Directory outputDirectory) async {
|
||||||
|
await _run('flutter', <String>['build', 'windows', '--release']);
|
||||||
|
final bundle = Directory('build/windows/x64/runner/Release');
|
||||||
|
if (!await bundle.exists()) {
|
||||||
|
throw StateError('No Windows release bundle found in ${bundle.path}.');
|
||||||
|
}
|
||||||
|
await _copyDirectory(
|
||||||
|
bundle, Directory(_join(outputDirectory.path, 'windows')));
|
||||||
|
await _run('dart', <String>['run', 'msix:create']);
|
||||||
|
final msixFiles = Directory.current
|
||||||
|
.listSync(recursive: true)
|
||||||
|
.whereType<File>()
|
||||||
|
.where((file) => file.path.endsWith('.msix'))
|
||||||
|
.toList(growable: false);
|
||||||
|
if (msixFiles.isEmpty) {
|
||||||
|
throw StateError('No .msix artifact produced by dart run msix:create.');
|
||||||
|
}
|
||||||
|
for (final file in msixFiles) {
|
||||||
|
await file.copy(_join(outputDirectory.path, _basename(file.path)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _buildLinux(Directory outputDirectory) async {
|
||||||
|
await _run('flutter', <String>['build', 'linux', '--release']);
|
||||||
|
await _run('appimage-builder', <String>[
|
||||||
|
'--recipe',
|
||||||
|
'AppImageBuilder.yml',
|
||||||
|
'--skip-test',
|
||||||
|
]);
|
||||||
|
final appImages = Directory.current
|
||||||
|
.listSync(recursive: true)
|
||||||
|
.whereType<File>()
|
||||||
|
.where((file) => file.path.endsWith('.AppImage'))
|
||||||
|
.toList(growable: false);
|
||||||
|
if (appImages.isEmpty) {
|
||||||
|
throw StateError('No AppImage artifact produced by appimage-builder.');
|
||||||
|
}
|
||||||
|
for (final file in appImages) {
|
||||||
|
await file.copy(_join(outputDirectory.path, _basename(file.path)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _run(String executable, List<String> arguments) async {
|
||||||
|
stdout.writeln('> $executable ${arguments.join(' ')}');
|
||||||
|
final result = await Process.run(
|
||||||
|
executable,
|
||||||
|
arguments,
|
||||||
|
runInShell: Platform.isWindows,
|
||||||
|
);
|
||||||
|
stdout.write(result.stdout);
|
||||||
|
stderr.write(result.stderr);
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
throw ProcessException(
|
||||||
|
executable,
|
||||||
|
arguments,
|
||||||
|
'Command failed with exit code ${result.exitCode}.',
|
||||||
|
result.exitCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _copyDirectory(Directory source, Directory destination) async {
|
||||||
|
await destination.create(recursive: true);
|
||||||
|
await for (final entity in source.list(recursive: false)) {
|
||||||
|
final targetPath = _join(destination.path, _basename(entity.path));
|
||||||
|
if (entity is Directory) {
|
||||||
|
await _copyDirectory(entity, Directory(targetPath));
|
||||||
|
} else if (entity is File) {
|
||||||
|
await entity.copy(targetPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _currentPlatform() {
|
||||||
|
if (Platform.isMacOS) return 'macos';
|
||||||
|
if (Platform.isWindows) return 'windows';
|
||||||
|
if (Platform.isLinux) return 'linux';
|
||||||
|
throw UnsupportedError('No Flutter desktop build target for this platform.');
|
||||||
|
}
|
||||||
|
|
||||||
|
String _basename(String path) {
|
||||||
|
final normalized = path.replaceAll('\\', '/');
|
||||||
|
return normalized.substring(normalized.lastIndexOf('/') + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _join(String first, String second) {
|
||||||
|
final separator = Platform.pathSeparator;
|
||||||
|
final left = first.endsWith(separator)
|
||||||
|
? first.substring(0, first.length - separator.length)
|
||||||
|
: first;
|
||||||
|
final right = second.startsWith(separator) ? second.substring(1) : second;
|
||||||
|
return '$left$separator$right';
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _option(List<String> arguments, String name) {
|
||||||
|
final index = arguments.indexOf(name);
|
||||||
|
if (index == -1 || index + 1 >= arguments.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return arguments[index + 1];
|
||||||
|
}
|
||||||
81
scripts/dev.dart
Normal file
81
scripts/dev.dart
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:scheduler_backup/backup.dart';
|
||||||
|
|
||||||
|
/// Starts local code generation and a Flutter desktop dev session.
|
||||||
|
Future<void> main(List<String> arguments) async {
|
||||||
|
final sqlitePath =
|
||||||
|
_option(arguments, '--sqlite') ?? defaultSchedulerSqliteFile().path;
|
||||||
|
final device = _option(arguments, '--device') ?? _defaultDesktopDevice();
|
||||||
|
|
||||||
|
stdout.writeln('SQLite path: $sqlitePath');
|
||||||
|
final buildRunner = await _start(
|
||||||
|
'dart',
|
||||||
|
<String>[
|
||||||
|
'run',
|
||||||
|
'build_runner',
|
||||||
|
'watch',
|
||||||
|
'--delete-conflicting-outputs',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
stdout.writeln('Started build_runner watch (pid ${buildRunner.pid}).');
|
||||||
|
|
||||||
|
_watchForExit(ProcessSignal.sigint, 130, buildRunner);
|
||||||
|
if (!Platform.isWindows) {
|
||||||
|
_watchForExit(ProcessSignal.sigterm, 143, buildRunner);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final flutter = await _start(
|
||||||
|
'flutter',
|
||||||
|
<String>[
|
||||||
|
'run',
|
||||||
|
'-d',
|
||||||
|
device,
|
||||||
|
'--dart-define=SCHEDULER_SQLITE_PATH=$sqlitePath',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
stdout.writeln(
|
||||||
|
'Started Flutter desktop with hot reload (pid ${flutter.pid}).');
|
||||||
|
exitCode = await flutter.exitCode;
|
||||||
|
} finally {
|
||||||
|
buildRunner.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _watchForExit(ProcessSignal signal, int code, Process buildRunner) {
|
||||||
|
signal.watch().listen((_) {
|
||||||
|
buildRunner.kill();
|
||||||
|
exit(code);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Process> _start(String executable, List<String> arguments) async {
|
||||||
|
stdout.writeln('> $executable ${arguments.join(' ')}');
|
||||||
|
try {
|
||||||
|
return Process.start(
|
||||||
|
executable,
|
||||||
|
arguments,
|
||||||
|
mode: ProcessStartMode.inheritStdio,
|
||||||
|
runInShell: Platform.isWindows,
|
||||||
|
);
|
||||||
|
} on ProcessException catch (error) {
|
||||||
|
stderr.writeln(error.message);
|
||||||
|
stderr.writeln('Unable to start: $executable');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _defaultDesktopDevice() {
|
||||||
|
if (Platform.isMacOS) return 'macos';
|
||||||
|
if (Platform.isWindows) return 'windows';
|
||||||
|
return 'linux';
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _option(List<String> arguments, String name) {
|
||||||
|
final index = arguments.indexOf(name);
|
||||||
|
if (index == -1 || index + 1 >= arguments.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return arguments[index + 1];
|
||||||
|
}
|
||||||
8
scripts/dev.sh
Executable file
8
scripts/dev.sh
Executable file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Compatibility wrapper for the Dart desktop development runner.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$root_dir"
|
||||||
|
|
||||||
|
exec dart run scripts/dev.dart "$@"
|
||||||
8
scripts/package_release.sh
Executable file
8
scripts/package_release.sh
Executable file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Compatibility wrapper for the Dart release packaging runner.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$root_dir"
|
||||||
|
|
||||||
|
exec dart run scripts/build.dart "$@"
|
||||||
53
scripts/test.dart
Normal file
53
scripts/test.dart
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
/// Runs tests, combines coverage, enforces coverage, and checks Dart docs.
|
||||||
|
Future<void> main(List<String> arguments) async {
|
||||||
|
await _deleteCoverageDirectory();
|
||||||
|
await _run('dart', <String>['pub', 'global', 'activate', 'coverage']);
|
||||||
|
await _run('dart', <String>['test', '--coverage=coverage']);
|
||||||
|
await _run('dart', <String>[
|
||||||
|
'pub',
|
||||||
|
'global',
|
||||||
|
'run',
|
||||||
|
'coverage:format_coverage',
|
||||||
|
'--lcov',
|
||||||
|
'--in=coverage',
|
||||||
|
'--out=coverage/lcov.info',
|
||||||
|
'--packages=.dart_tool/package_config.json',
|
||||||
|
'--report-on=packages',
|
||||||
|
]);
|
||||||
|
await _run('dart', <String>[
|
||||||
|
'run',
|
||||||
|
'tool/check_coverage.dart',
|
||||||
|
'coverage/lcov.info',
|
||||||
|
'80',
|
||||||
|
]);
|
||||||
|
await _run('dart', <String>['run', 'tool/check_docs.dart']);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteCoverageDirectory() async {
|
||||||
|
final coverage = Directory('coverage');
|
||||||
|
if (await coverage.exists()) {
|
||||||
|
await coverage.delete(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _run(String executable, List<String> arguments) async {
|
||||||
|
final command = '$executable ${arguments.join(' ')}';
|
||||||
|
stdout.writeln('> $command');
|
||||||
|
final result = await Process.run(
|
||||||
|
executable,
|
||||||
|
arguments,
|
||||||
|
runInShell: Platform.isWindows,
|
||||||
|
);
|
||||||
|
stdout.write(result.stdout);
|
||||||
|
stderr.write(result.stderr);
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
throw ProcessException(
|
||||||
|
executable,
|
||||||
|
arguments,
|
||||||
|
'Command failed with exit code ${result.exitCode}.',
|
||||||
|
result.exitCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
scripts/test.sh
Executable file
5
scripts/test.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
dart analyze
|
||||||
|
dart run scripts/test.dart
|
||||||
82
tool/check_coverage.dart
Normal file
82
tool/check_coverage.dart
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
/// Checks an LCOV report against a package-library coverage threshold.
|
||||||
|
void main(List<String> arguments) {
|
||||||
|
if (arguments.length != 2) {
|
||||||
|
stderr.writeln('Usage: dart run tool/check_coverage.dart <lcov> <percent>');
|
||||||
|
exitCode = 64;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final lcovFile = File(arguments[0]);
|
||||||
|
final threshold = double.tryParse(arguments[1]);
|
||||||
|
if (threshold == null) {
|
||||||
|
stderr.writeln('Coverage threshold must be numeric: ${arguments[1]}');
|
||||||
|
exitCode = 64;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!lcovFile.existsSync()) {
|
||||||
|
stderr.writeln('Coverage file not found: ${lcovFile.path}');
|
||||||
|
exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final coverage = _readCoverage(lcovFile);
|
||||||
|
if (coverage.totalLines == 0) {
|
||||||
|
stderr.writeln('Coverage file has no package lib/ lines.');
|
||||||
|
exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final percent = coverage.percent;
|
||||||
|
stdout.writeln(
|
||||||
|
'Package lib coverage: ${percent.toStringAsFixed(2)}% '
|
||||||
|
'(${coverage.coveredLines}/${coverage.totalLines})',
|
||||||
|
);
|
||||||
|
if (percent < threshold) {
|
||||||
|
stderr.writeln(
|
||||||
|
'Coverage ${percent.toStringAsFixed(2)}% is below '
|
||||||
|
'${threshold.toStringAsFixed(2)}%.',
|
||||||
|
);
|
||||||
|
exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
({int coveredLines, int totalLines, double percent}) _readCoverage(File file) {
|
||||||
|
var includeFile = false;
|
||||||
|
var coveredLines = 0;
|
||||||
|
var totalLines = 0;
|
||||||
|
|
||||||
|
for (final line in file.readAsLinesSync()) {
|
||||||
|
if (line.startsWith('SF:')) {
|
||||||
|
includeFile = _includeSourceFile(line.substring(3));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!includeFile || !line.startsWith('DA:')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final comma = line.indexOf(',');
|
||||||
|
if (comma == -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
totalLines += 1;
|
||||||
|
final hits = int.tryParse(line.substring(comma + 1).split(',').first) ?? 0;
|
||||||
|
if (hits > 0) {
|
||||||
|
coveredLines += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final percent = totalLines == 0 ? 0.0 : coveredLines * 100 / totalLines;
|
||||||
|
return (
|
||||||
|
coveredLines: coveredLines,
|
||||||
|
totalLines: totalLines,
|
||||||
|
percent: percent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _includeSourceFile(String path) {
|
||||||
|
final normalized = path.replaceAll('\\', '/');
|
||||||
|
return normalized.contains('/packages/') &&
|
||||||
|
normalized.contains('/lib/') &&
|
||||||
|
!normalized.endsWith('.g.dart');
|
||||||
|
}
|
||||||
56
tool/check_docs.dart
Normal file
56
tool/check_docs.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
/// Generates Dart docs for each package with public libraries and fails on warnings.
|
||||||
|
Future<void> main(List<String> arguments) async {
|
||||||
|
final outputRoot = Directory(_join(_join('doc', 'api'), 'packages'));
|
||||||
|
if (await outputRoot.exists()) {
|
||||||
|
await outputRoot.delete(recursive: true);
|
||||||
|
}
|
||||||
|
await outputRoot.create(recursive: true);
|
||||||
|
|
||||||
|
final packages = Directory('packages')
|
||||||
|
.listSync()
|
||||||
|
.whereType<Directory>()
|
||||||
|
.where((directory) {
|
||||||
|
return File(_join(directory.path, 'pubspec.yaml')).existsSync() &&
|
||||||
|
Directory(_join(directory.path, 'lib')).existsSync();
|
||||||
|
}).toList(growable: false)
|
||||||
|
..sort((left, right) => left.path.compareTo(right.path));
|
||||||
|
|
||||||
|
var failed = false;
|
||||||
|
for (final package in packages) {
|
||||||
|
final outputPath = _join(outputRoot.absolute.path, _basename(package.path));
|
||||||
|
stdout.writeln('> dart doc --output $outputPath (${package.path})');
|
||||||
|
final result = await Process.run(
|
||||||
|
'dart',
|
||||||
|
<String>['doc', '--output', outputPath],
|
||||||
|
workingDirectory: package.path,
|
||||||
|
runInShell: Platform.isWindows,
|
||||||
|
);
|
||||||
|
final output = '${result.stdout}${result.stderr}';
|
||||||
|
stdout.write(output);
|
||||||
|
if (result.exitCode != 0 || _hasWarnings(output)) {
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed) {
|
||||||
|
stderr.writeln('Dart documentation gate failed.');
|
||||||
|
exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasWarnings(String output) {
|
||||||
|
return output.contains(' warning:') ||
|
||||||
|
RegExp(r'Found [1-9][0-9]* warnings').hasMatch(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _join(String first, String second) {
|
||||||
|
final separator = Platform.pathSeparator;
|
||||||
|
return '$first$separator$second';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _basename(String path) {
|
||||||
|
final normalized = path.replaceAll('\\', '/');
|
||||||
|
return normalized.substring(normalized.lastIndexOf('/') + 1);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue