Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,15 @@ concurrency:

jobs:
# ─────────────────────────────────────────────────────────────────────────────
# Unit Tests - Run first for fast feedback
# Initial quality gate - all matrix jobs wait for this job to pass
# ─────────────────────────────────────────────────────────────────────────────
test:
name: Test
timeout-minutes: 30
runs-on: ubuntu-latest
env:
DCM_CI_KEY: ${{ secrets.DCM_CI_KEY }}
DCM_EMAIL: ${{ secrets.DCM_EMAIL }}
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -71,26 +74,22 @@ jobs:
working-directory: fvm_mcp
run: dart pub get

- uses: invertase/github-action-dart-analyzer@v1
with:
fatal-infos: false
- name: Analyze
run: dart analyze --fatal-infos

# DCM >=1.38.2 refuses to run on CI without a license (DCM_CI_KEY and
# DCM_EMAIL). Skip the steps when the repo has no license secrets; the
# pre-commit and pre-push hooks still run DCM locally.
- name: Install DCM
if: env.DCM_CI_KEY != '' && env.DCM_EMAIL != ''
uses: CQLabs/setup-dcm@v1
continue-on-error: true
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

- name: Ensure DCM available
run: |
if ! command -v dcm >/dev/null 2>&1; then
dart pub global activate dart_code_metrics
echo "$HOME/.pub-cache/bin" >> "$GITHUB_PATH"
fi

- name: Run DCM
run: dcm analyze lib fvm_mcp/lib

if: env.DCM_CI_KEY != '' && env.DCM_EMAIL != ''
run: dcm analyze lib

- name: Install lcov
run: sudo apt-get install lcov

Expand Down Expand Up @@ -141,6 +140,7 @@ jobs:
name: Test on ${{ matrix.os }}
timeout-minutes: 45
runs-on: ${{ matrix.os }}
needs: test
strategy:
fail-fast: true
matrix:
Expand Down Expand Up @@ -174,12 +174,12 @@ jobs:
shell: bash

# ─────────────────────────────────────────────────────────────────────────────
# Integration Tests - Run in parallel with unit tests
# Matrix checks - fan out after the initial quality gate
# ─────────────────────────────────────────────────────────────────────────────
integration-test:
name: Integration Tests on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
needs: [test, test-os]
needs: test
timeout-minutes: 45
# Skip heavy integration tests for the fvm-mcp PR branch (MCP-only changes).
if: github.event_name != 'pull_request' || github.event.pull_request.head.ref != 'fvm-mcp'
Expand Down Expand Up @@ -213,6 +213,7 @@ jobs:
migration-test:
name: Migration Test on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
needs: test
timeout-minutes: 60

strategy:
Expand Down
515 changes: 515 additions & 0 deletions internal/fvm-5-binary-migration-plan.md

Large diffs are not rendered by default.

7 changes: 3 additions & 4 deletions lib/src/commands/doctor_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,11 @@ class DoctorCommand extends BaseFvmCommand {
);

final sdkPath = settings['dart.flutterSdkPath'];
final matchesPinnedVersion = sdkPath is String &&
convertToPosixPath(sdkPath) == expectedSdkPath;

table.insertRow(['dart.flutterSdkPath', sdkPath ?? 'None']);
table.insertRow([
'Matches pinned version:',
sdkPath == expectedSdkPath,
]);
table.insertRow(['Matches pinned version:', matchesPinnedVersion]);
} on FormatException catch (_, stackTrace) {
logger
..err('Error parsing Vscode settings.json on ${settingsFile.path}')
Expand Down
82 changes: 45 additions & 37 deletions lib/src/runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:args/command_runner.dart';
import 'package:cli_completion/cli_completion.dart';
import 'package:io/ansi.dart';
import 'package:io/io.dart';
import 'package:pub_updater/pub_updater.dart';
import 'package:pub_semver/pub_semver.dart';

import 'commands/api_command.dart';
import 'commands/config_command.dart';
Expand All @@ -27,6 +27,7 @@ import 'commands/spawn_command.dart';
import 'commands/use_command.dart';
import 'models/config_model.dart';
import 'models/log_level_model.dart';
import 'services/fvm_release_service.dart';
import 'services/logger_service.dart';
import 'utils/constants.dart';
import 'utils/context.dart';
Expand All @@ -36,11 +37,12 @@ import 'version.dart';
/// Command Runner for FVM
class FvmCommandRunner extends CompletionCommandRunner<int> {
final FvmContext context;
final PubUpdater _pubUpdater;
final FvmReleaseService _releaseService;
static const _updateCheckInterval = Duration(days: 1);

/// Constructor
FvmCommandRunner(this.context, {PubUpdater? pubUpdater})
: _pubUpdater = pubUpdater ?? PubUpdater(),
FvmCommandRunner(this.context, {FvmReleaseService? releaseService})
: _releaseService = releaseService ?? context.get<FvmReleaseService>(),
super(kPackageName, kDescription) {
argParser
..addFlag('verbose', help: 'Print verbose output.', negatable: false)
Expand Down Expand Up @@ -69,49 +71,55 @@ class FvmCommandRunner extends CompletionCommandRunner<int> {
addCommand(IntegrationTestCommand(context));
}

/// Checks if the current version (set by the build runner on the
/// version.dart file) is the most recent one. If not, show a prompt to the
/// user.
Future<Function()?> _checkForUpdates() async {
/// Returns a notice when a newer stable FVM release is available.
Future<void Function()?> _checkForUpdates() async {
try {
final lastUpdateCheck = context.lastUpdateCheck ?? DateTime.now();
if (context.updateCheckDisabled) return null;
final oneDay = lastUpdateCheck.add(const Duration(days: 1));

if (DateTime.now().isBefore(oneDay)) {
final now = DateTime.now();
final lastUpdateCheck = context.lastUpdateCheck;
if (lastUpdateCheck != null &&
now.isBefore(lastUpdateCheck.add(_updateCheckInterval))) {
return null;
}

LocalAppConfig.read(path: context.appConfigPath, requireValid: true)
..lastUpdateCheck = DateTime.now()
..lastUpdateCheck = now
..save(path: context.appConfigPath);

final isUpToDate = await _pubUpdater.isUpToDate(
packageName: kPackageName,
currentVersion: packageVersion,
);
final latestRelease = await _releaseService.getLatestStableRelease();
final currentVersion = Version.parse(packageVersion);
if (latestRelease.version.compareTo(currentVersion) <= 0) return null;

if (isUpToDate) return null;
return () => _showUpdateNotice(latestRelease, currentVersion);
} catch (_) {
return () => logger.debug('Failed to check for updates.');
}
}

final latestVersion = await _pubUpdater.getLatestVersion(kPackageName);
void _showUpdateNotice(FvmRelease latestRelease, Version currentVersion) {
final updateAvailableLabel = lightYellow.wrap('Update available!');
final currentVersionLabel = lightCyan.wrap(packageVersion);
final latestVersionLabel = lightCyan.wrap(
latestRelease.version.toString(),
);

return () {
final updateAvailableLabel = lightYellow.wrap('Update available!');
final currentVersionLabel = lightCyan.wrap(packageVersion);
final latestVersionLabel = lightCyan.wrap(latestVersion);
logger
..info()
..info(
'$updateAvailableLabel $currentVersionLabel \u2192 $latestVersionLabel',
)
..info();

logger
..info()
..info(
'$updateAvailableLabel $currentVersionLabel \u2192 $latestVersionLabel',
)
..info();
};
} catch (_) {
return () {
logger.debug("Failed to check for updates.");
};
}
if (latestRelease.version.major <= currentVersion.major) return;

logger
..info(
'FVM ${latestRelease.version.major} is distributed as a standalone '
'CLI and is no longer published to pub.dev.',
)
..info('Migration guide: $kFvmMigrationGuideUrl')
..info();
}

/// Checks for deprecated environment variables and shows warnings
Expand Down Expand Up @@ -281,7 +289,7 @@ class FvmCommandRunner extends CompletionCommandRunner<int> {
// Check for deprecated environment variables
_checkDeprecatedEnvironmentVariables();

final checkingForUpdate = _checkForUpdates();
final updateCheck = _checkForUpdates();

// Run the command or show version
final int? exitCode;
Expand All @@ -292,8 +300,8 @@ class FvmCommandRunner extends CompletionCommandRunner<int> {
exitCode = await super.runCommand(topLevelResults);
}

final logOutput = await checkingForUpdate;
logOutput?.call();
final updateNotice = await updateCheck;
updateNotice?.call();

return exitCode;
}
Expand Down
Loading
Loading