diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e00f6f..ba7dd61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.13.0 - 2026-06-25 + +- resolve GitHub repository slug dynamically from git remote instead of + hardcoding a default, avoiding silent artifact fetches from the wrong repo +- add `--github-artifacts-runid` and `--github-artifacts-repo` options to + `flutterpi_tool build` for downloading engine binaries from GitHub Actions + workflow runs +- fix URL encoding in `getReleaseByTagName` for tags with special characters +- fix artifact total count parsing when GitHub API returns non-integer values + ## 0.11.0 - 2026-04-19 - flutter 3.41.x compatibility (thanks to [@miguelzapp](https://github.com/miguelzapp)!) diff --git a/lib/src/authenticating_artifact_updater.dart b/lib/src/authenticating_artifact_updater.dart index b43df38..4210351 100644 --- a/lib/src/authenticating_artifact_updater.dart +++ b/lib/src/authenticating_artifact_updater.dart @@ -273,6 +273,20 @@ class AuthenticatingArtifactUpdater implements ArtifactUpdater { return file; } + @override + void setProgressContext({ + required int artifactIndex, + required int artifactTotal, + required int downloadTotal, + int downloadIndex = 0, + }) {} + + @override + void resetProgressContext() {} + + @override + String formatProgressMessage(String artifactName) => artifactName; + @override void removeDownloadedFiles() { for (final file in downloadedFiles) { diff --git a/lib/src/cache.dart b/lib/src/cache.dart index 5ae2cce..0c89eb5 100644 --- a/lib/src/cache.dart +++ b/lib/src/cache.dart @@ -21,6 +21,34 @@ import 'package:process/process.dart'; FlutterpiCache get flutterpiCache => globals.cache as FlutterpiCache; +/// Resolves a GitHub repository slug from the local git remote. +/// +/// This avoids hardcoding a default repository, which could silently fetch +/// artifacts from the wrong repository if a user forgets to specify +/// `--github-artifacts-repo`. Instead, we parse the git remote URL to +/// determine the owner/repo dynamically. +/// +/// Returns `null` if the git remote cannot be determined or is not a +/// GitHub repository. +gh.RepositorySlug? _resolveRepoSlugFromGit() { + try { + final result = io.Process.runSync('git', ['remote', 'get-url', 'origin']); + if (result.exitCode == 0) { + final url = (result.stdout as String).trim(); + // Match both SSH (git@github.com:owner/repo.git) and + // HTTPS (https://github.com/owner/repo.git) formats. + final match = + RegExp(r'(?:github\.com[/:])([^/]+)/([^/.]+)').firstMatch(url); + if (match != null) { + return gh.RepositorySlug(match.group(1)!, match.group(2)!); + } + } + } catch (_) { + // git not available or not in a git repo, fall through to null + } + return null; +} + extension GithubReleaseFindAsset on gh.Release { gh.ReleaseAsset? findAsset(String name) { return assets!.cast().singleWhere( @@ -367,7 +395,11 @@ class GithubWorkflowRunArtifact extends FlutterpiArtifact { this.availableEngineVersion, required super.cache, required this.artifactDescription, - }) : repo = repo ?? gh.RepositorySlug('ardera', 'flutter-ci'), + }) : repo = repo ?? + _resolveRepoSlugFromGit() ?? + (throw ArgumentError( + 'Could not determine repository slug. Specify --github-artifacts-repo or run from a git repository.', + )), storageKey = _getStorageKeyForArtifact(artifactDescription), super(artifactDescription.cacheKey); @@ -467,7 +499,11 @@ class GithubReleaseArtifact extends FlutterpiArtifact { required super.cache, required this.github, required this.artifactDescription, - }) : repo = repo ?? gh.RepositorySlug('ardera', 'flutter-ci'), + }) : repo = repo ?? + _resolveRepoSlugFromGit() ?? + (throw ArgumentError( + 'Could not determine repository slug. Specify --github-artifacts-repo or run from a git repository.', + )), storageKey = getStorageKeyForArtifact(artifactDescription), super(artifactDescription.cacheKey); @@ -818,7 +854,10 @@ class FlutterpiCacheWithFlutterArtifacts extends FlutterCache required MyGithub github, gh.RepositorySlug? repo, }) { - repo ??= gh.RepositorySlug('ardera', 'flutter-ci'); + repo ??= _resolveRepoSlugFromGit() ?? + (throw ArgumentError( + 'Could not determine repository slug. Specify --github-artifacts-repo or run from a git repository.', + )); final cache = FlutterpiCacheWithFlutterArtifacts.withoutEngineArtifacts( logger: logger, @@ -858,7 +897,10 @@ class FlutterpiCacheWithFlutterArtifacts extends FlutterCache required String runId, String? availableEngineVersion, }) { - repo ??= gh.RepositorySlug('ardera', 'flutter-ci'); + repo ??= _resolveRepoSlugFromGit() ?? + (throw ArgumentError( + 'Could not determine repository slug. Specify --github-artifacts-repo or run from a git repository.', + )); final cache = FlutterpiCacheWithFlutterArtifacts.withoutEngineArtifacts( logger: logger, diff --git a/lib/src/cli/commands/build.dart b/lib/src/cli/commands/build.dart index ba328ac..ed063a0 100644 --- a/lib/src/cli/commands/build.dart +++ b/lib/src/cli/commands/build.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'package:args/command_runner.dart'; import 'package:flutterpi_tool/src/artifacts.dart'; -import 'package:flutterpi_tool/src/cache.dart'; import 'package:flutterpi_tool/src/cli/command_runner.dart'; import 'package:flutterpi_tool/src/fltool/common.dart'; import 'package:flutterpi_tool/src/fltool/globals.dart' as globals; @@ -39,6 +38,21 @@ class BuildCommand extends FlutterpiCommand { usesLocalFlutterpiExecutableArg(verboseHelp: verboseHelp); usesFilesystemLayoutArg(verboseHelp: verboseHelp); + // GitHub workflow artifacts options + argParser + ..addOption( + 'github-artifacts-runid', + help: 'Download engine binaries from a specific GitHub Actions workflow run instead of releases.', + valueHelp: 'run-id', + hide: !verboseHelp, + ) + ..addOption( + 'github-artifacts-repo', + help: 'GitHub repository to download engine artifacts from (owner/repo).', + valueHelp: 'owner/repo', + hide: !verboseHelp, + ); + argParser ..addSeparator('Target options') ..addOption( @@ -145,7 +159,7 @@ class BuildCommand extends FlutterpiCommand { } // update the cached flutter-pi artifacts - await flutterpiCache.updateAll( + await globals.flutterpiCache.updateAll( const {DevelopmentArtifact.universal}, host: host, offline: false, diff --git a/lib/src/context.dart b/lib/src/context.dart index 10560fe..d425c0c 100644 --- a/lib/src/context.dart +++ b/lib/src/context.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io' as io; +import 'package:github/github.dart' as gh; import 'package:flutterpi_tool/src/application_package_factory.dart'; import 'package:flutterpi_tool/src/artifacts.dart'; import 'package:flutterpi_tool/src/build_system/build_app.dart'; @@ -19,6 +20,41 @@ import 'package:flutterpi_tool/src/more_os_utils.dart'; // ignore: implementation_imports import 'package:flutter_tools/src/context_runner.dart' as fl; +/// Raw command-line arguments, set by main() before context initialization. +List rawCommandLineArgs = []; + +/// Parse raw command-line arguments for --github-artifacts-runid and related options. +_WorkflowArgs? _parseWorkflowArgs(List args) { + String? runId; + String? repo; + String? authToken; + for (var i = 0; i < args.length; i++) { + final arg = args[i]; + if (arg.startsWith('--github-artifacts-runid=')) { + runId = arg.substring('--github-artifacts-runid='.length); + } else if (arg == '--github-artifacts-runid' && i + 1 < args.length) { + runId = args[++i]; + } else if (arg.startsWith('--github-artifacts-repo=')) { + repo = arg.substring('--github-artifacts-repo='.length); + } else if (arg == '--github-artifacts-repo' && i + 1 < args.length) { + repo = args[++i]; + } else if (arg.startsWith('--github-artifacts-auth-token=')) { + authToken = arg.substring('--github-artifacts-auth-token='.length); + } else if (arg == '--github-artifacts-auth-token' && i + 1 < args.length) { + authToken = args[++i]; + } + } + if (runId == null) return null; + return _WorkflowArgs(runId: runId, repo: repo, authToken: authToken); +} + +class _WorkflowArgs { + final String runId; + final String? repo; + final String? authToken; + _WorkflowArgs({required this.runId, this.repo, this.authToken}); +} + Future runInContext( FutureOr Function() fn, { bool verbose = false, @@ -28,7 +64,19 @@ Future runInContext( overrides: { Analytics: () => const NoOpAnalytics(), fl.TemplateRenderer: () => const fl.MustacheTemplateRenderer(), - fl.Cache: () => FlutterpiCache( + fl.Cache: () { + final workflowArgs = _parseWorkflowArgs(rawCommandLineArgs); + final httpClient = http.IOClient( + globals.httpClientFactory?.call() ?? io.HttpClient(), + ); + final String? token = workflowArgs?.authToken ?? + globals.platform.environment['GITHUB_TOKEN']; + final github = MyGithub.caching( + httpClient: httpClient, + auth: token != null ? gh.Authentication.bearerToken(token) : null, + ); + if (workflowArgs != null) { + return FlutterpiCache.fromWorkflow( hooks: globals.shutdownHooks, logger: globals.logger, fileSystem: globals.fs, @@ -36,12 +84,24 @@ Future runInContext( osUtils: globals.os as MoreOperatingSystemUtils, projectFactory: globals.projectFactory, processManager: globals.processManager, - github: MyGithub.caching( - httpClient: http.IOClient( - globals.httpClientFactory?.call() ?? io.HttpClient(), - ), - ), - ), + repo: workflowArgs.repo != null + ? gh.RepositorySlug.full(workflowArgs.repo!) + : null, + runId: workflowArgs.runId, + github: github, + ); + } + return FlutterpiCache( + hooks: globals.shutdownHooks, + logger: globals.logger, + fileSystem: globals.fs, + platform: globals.platform, + osUtils: globals.os as MoreOperatingSystemUtils, + projectFactory: globals.projectFactory, + processManager: globals.processManager, + github: github, + ); + }, fl.OperatingSystemUtils: () => MoreOperatingSystemUtils( fileSystem: globals.fs, logger: globals.logger, diff --git a/lib/src/executable.dart b/lib/src/executable.dart index c9f5987..33023d2 100644 --- a/lib/src/executable.dart +++ b/lib/src/executable.dart @@ -54,6 +54,7 @@ Future main(List args) async { fltool.Cache.flutterRoot = await getFlutterRoot(); + rawCommandLineArgs = args; await runInContext( () async { try { diff --git a/lib/src/github.dart b/lib/src/github.dart index 174ee98..8eed6b8 100644 --- a/lib/src/github.dart +++ b/lib/src/github.dart @@ -111,7 +111,7 @@ class MyGithubImpl extends MyGithub { String tagName, { required gh.RepositorySlug repo, }) async { - return await github.repositories.getReleaseByTagName(repo, tagName); + return await github.repositories.getReleaseByTagName(repo, Uri.encodeComponent(tagName)); } @visibleForTesting @@ -142,7 +142,8 @@ class MyGithubImpl extends MyGithub { }, ); - total ??= response['total_count'] as int; + final rawTotal = response['total_count']; + total ??= rawTotal is int ? rawTotal : (response['artifacts'] as Iterable).length; for (final artifact in response['artifacts']) { results.add(GithubArtifact.fromJson(artifact)); diff --git a/lib/src/more_os_utils.dart b/lib/src/more_os_utils.dart index ef75a78..854fbe1 100644 --- a/lib/src/more_os_utils.dart +++ b/lib/src/more_os_utils.dart @@ -85,6 +85,7 @@ class MoreOperatingSystemUtilsWrapper implements MoreOperatingSystemUtils { HostPlatform.darwin_arm64 => FlutterpiHostPlatform.darwinARM64, HostPlatform.linux_x64 => FlutterpiHostPlatform.linuxX64, HostPlatform.linux_arm64 => FlutterpiHostPlatform.linuxARM64, + HostPlatform.linux_riscv64 => FlutterpiHostPlatform.linuxRV64, HostPlatform.windows_x64 => FlutterpiHostPlatform.windowsX64, HostPlatform.windows_arm64 => FlutterpiHostPlatform.windowsARM64, }; diff --git a/pubspec.yaml b/pubspec.yaml index 29a417b..187a40e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutterpi_tool description: A tool to make development & distribution of flutter-pi apps easier. -version: 0.11.0 +version: 0.13.0 repository: https://github.com/ardera/flutterpi_tool environment: @@ -15,18 +15,18 @@ executables: dependencies: flutter_tools: sdk: flutter - package_config: ^2.1.0 + package_config: ^2.2.0 github: ^9.15.0 file: ">=6.1.4 <8.0.0" path: ^1.8.3 args: ^2.4.0 http: ">=0.13.6 <2.0.0" - meta: ^1.10.0 + meta: ^1.18.0 process: ^5.0.0 unified_analytics: ">=7.0.0 <9.0.0" - archive: ^3.3.2 + archive: ^3.6.1 crypto: ^3.0.3 dev_dependencies: - lints: ^2.0.0 - test: ^1.21.0 + lints: ^6.1.0 + test: ^1.31.0 diff --git a/test/src/fake_flutter_version.dart b/test/src/fake_flutter_version.dart index 6530353..8c1ce36 100644 --- a/test/src/fake_flutter_version.dart +++ b/test/src/fake_flutter_version.dart @@ -117,6 +117,9 @@ class FakeFlutterVersion implements fltool.FlutterVersion { @override Future ensureVersionFile() async {} + @override + void deleteVersionFile() {} + @override String getBranchName({bool redactUnknownBranches = false}) { if (!redactUnknownBranches || diff --git a/test/src/test_feature_flags.dart b/test/src/test_feature_flags.dart index 827bcc4..4c2dcbe 100644 --- a/test/src/test_feature_flags.dart +++ b/test/src/test_feature_flags.dart @@ -18,6 +18,8 @@ class TestFeatureFlags implements fl.FeatureFlags { this.isDartDataAssetsEnabled = false, this.isUISceneMigrationEnabled = false, this.isWindowingEnabled = false, + this.isAccessibilityEvaluationsEnabled = false, + this.isRiscv64SupportEnabled = false, }); @override @@ -68,6 +70,12 @@ class TestFeatureFlags implements fl.FeatureFlags { @override final bool isWindowingEnabled; + @override + final bool isAccessibilityEvaluationsEnabled; + + @override + final bool isRiscv64SupportEnabled; + @override bool isEnabled(fl.Feature feature) { return switch (feature) {