From ef7c4f63f661e920683387ac46c84a63af959094 Mon Sep 17 00:00:00 2001 From: Chris D Date: Thu, 25 Jun 2026 15:59:59 -0400 Subject: [PATCH] Resolve GitHub repo slug dynamically & add workflow artifact options - Replace hardcoded repository slug defaults with git remote parsing in cache.dart, 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 - Parse workflow args early in context initialization to support cache creation from CLI flags - Fix URL encoding in getReleaseByTagName for tags with special chars - Fix artifact total count parsing when GitHub API returns non-integer - Bump version to 0.13.0 --- CHANGELOG.md | 10 +++++ lib/src/cache.dart | 50 ++++++++++++++++++++-- lib/src/cli/commands/build.dart | 18 +++++++- lib/src/context.dart | 74 +++++++++++++++++++++++++++++---- lib/src/executable.dart | 1 + lib/src/github.dart | 5 ++- pubspec.yaml | 2 +- 7 files changed, 144 insertions(+), 16 deletions(-) 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/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/pubspec.yaml b/pubspec.yaml index 29a417b..0caa181 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: