diff --git a/lib/src/cli/commands/create_release_command.dart b/lib/src/cli/commands/create_release_command.dart index f99fb40..6348b90 100644 --- a/lib/src/cli/commands/create_release_command.dart +++ b/lib/src/cli/commands/create_release_command.dart @@ -235,6 +235,7 @@ class CreateReleaseCommand extends Command { } // Step 4: Commit all changes + Logger.info('Configuring git identity for release commit'); CiProcessRunner.exec('git', ['config', 'user.name', 'github-actions[bot]'], cwd: repoRoot, verbose: global.verbose); CiProcessRunner.exec( 'git', @@ -259,6 +260,7 @@ class CreateReleaseCommand extends Command { if (Directory('$repoRoot/$kCicdAuditDir').existsSync()) { filesToAdd.add('$kCicdAuditDir/'); } + Logger.info('Staging ${filesToAdd.length} release artifacts for commit'); for (final path in filesToAdd) { final fullPath = '$repoRoot/$path'; if (File(fullPath).existsSync() || Directory(fullPath).existsSync()) { @@ -292,6 +294,7 @@ class CreateReleaseCommand extends Command { final ghToken = Platform.environment['GH_TOKEN'] ?? Platform.environment['GITHUB_TOKEN']; final remoteRepo = Platform.environment['GITHUB_REPOSITORY'] ?? effectiveRepo; if (ghToken != null && remoteRepo.isNotEmpty) { + Logger.info('Setting authenticated remote URL for push'); CiProcessRunner.exec( 'git', ['remote', 'set-url', 'origin', 'https://x-access-token:$ghToken@github.com/$remoteRepo.git'], diff --git a/lib/src/cli/manage_cicd.dart b/lib/src/cli/manage_cicd.dart index adda721..e77a8d3 100644 --- a/lib/src/cli/manage_cicd.dart +++ b/lib/src/cli/manage_cicd.dart @@ -2016,6 +2016,7 @@ Future _runCreateRelease(String repoRoot, List args) async { } // Step 4: Commit all changes + _info('Configuring git identity for release commit'); _exec('git', ['config', 'user.name', 'github-actions[bot]'], cwd: repoRoot); _exec('git', ['config', 'user.email', 'github-actions[bot]@users.noreply.github.com'], cwd: repoRoot); @@ -2033,6 +2034,7 @@ Future _runCreateRelease(String repoRoot, List args) async { ]; if (Directory('$repoRoot/docs').existsSync()) filesToAdd.add('docs/'); if (Directory('$repoRoot/$kCicdAuditDir').existsSync()) filesToAdd.add('$kCicdAuditDir/'); + _info('Staging ${filesToAdd.length} release artifacts for commit'); for (final path in filesToAdd) { final fullPath = '$repoRoot/$path'; if (File(fullPath).existsSync() || Directory(fullPath).existsSync()) { diff --git a/lib/src/cli/utils/process_runner.dart b/lib/src/cli/utils/process_runner.dart index a451ab2..d15aec0 100644 --- a/lib/src/cli/utils/process_runner.dart +++ b/lib/src/cli/utils/process_runner.dart @@ -4,6 +4,27 @@ import 'logger.dart'; /// Utilities for running external processes. abstract final class CiProcessRunner { + /// Patterns that look like tokens/secrets — redact before logging. + static final _secretPatterns = [ + // GitHub PATs (classic and fine-grained) + RegExp(r'ghp_[A-Za-z0-9]{36,}'), + RegExp(r'github_pat_[A-Za-z0-9_]{80,}'), + // Generic long hex/base64 strings that follow "token" or auth keywords + RegExp(r'(?<=token[=: ])[A-Za-z0-9_\-]{20,}', caseSensitive: false), + RegExp(r'(?<=bearer )[A-Za-z0-9_\-\.]{20,}', caseSensitive: false), + // URLs with embedded credentials (https://user:TOKEN@host) + RegExp(r'(?<=:)[A-Za-z0-9_\-]{20,}(?=@github\.com)'), + ]; + + /// Redact secrets from a string before logging. + static String _redact(String input) { + var result = input; + for (final pattern in _secretPatterns) { + result = result.replaceAll(pattern, '***REDACTED***'); + } + return result; + } + /// Check whether a command is available on the system PATH. static bool commandExists(String command) { try { @@ -17,7 +38,7 @@ abstract final class CiProcessRunner { /// Run a shell command synchronously and return trimmed stdout. static String runSync(String command, String workingDirectory, {bool verbose = false}) { - if (verbose) Logger.info('[CMD] $command'); + if (verbose) Logger.info('[CMD] ${_redact(command)}'); final result = Process.runSync('sh', ['-c', command], workingDirectory: workingDirectory); final output = (result.stdout as String).trim(); if (verbose && output.isNotEmpty) Logger.info(' $output'); @@ -26,7 +47,7 @@ abstract final class CiProcessRunner { /// Execute a command. Set [fatal] to true to exit on failure. static void exec(String executable, List args, {String? cwd, bool fatal = false, bool verbose = false}) { - if (verbose) Logger.info(' \$ $executable ${args.join(" ")}'); + if (verbose) Logger.info(' \$ ${_redact('$executable ${args.join(" ")}')}'); final result = Process.runSync(executable, args, workingDirectory: cwd); if (result.exitCode != 0) { Logger.error(' Command failed (exit ${result.exitCode}): ${result.stderr}'); diff --git a/lib/src/cli/utils/release_utils.dart b/lib/src/cli/utils/release_utils.dart index d2bce81..d34ed66 100644 --- a/lib/src/cli/utils/release_utils.dart +++ b/lib/src/cli/utils/release_utils.dart @@ -4,7 +4,6 @@ import 'dart:io'; import '../../triage/utils/config.dart'; import '../../triage/utils/run_context.dart'; import 'logger.dart'; -import 'process_runner.dart'; /// Utilities for release management. abstract final class ReleaseUtils { @@ -75,17 +74,18 @@ abstract final class ReleaseUtils { } buf.writeln(); } - } catch (_) { - // Skip if parse fails + } catch (e) { + Logger.warn('Could not parse contributors.json: $e'); } } - // Commit range - final commitCount = CiProcessRunner.runSync( - 'git rev-list --count "$prevTag"..HEAD 2>/dev/null', - repoRoot, - verbose: verbose, - ); + // Commit range — use array args to avoid shell injection via prevTag. + final commitCountResult = Process.runSync('git', [ + 'rev-list', + '--count', + '$prevTag..HEAD', + ], workingDirectory: repoRoot); + final commitCount = commitCountResult.exitCode == 0 ? (commitCountResult.stdout as String).trim() : '?'; buf.writeln('---'); buf.writeln('Automated release by CI/CD pipeline (Gemini CLI + GitHub Actions)'); buf.writeln('Commits since $prevTag: $commitCount'); @@ -98,10 +98,14 @@ abstract final class ReleaseUtils { static List> gatherVerifiedContributors(String repoRoot, String prevTag) { final repo = Platform.environment['GITHUB_REPOSITORY'] ?? '${config.repoOwner}/${config.repoName}'; - // Step 1: Get one commit SHA per unique author email - final gitResult = Process.runSync('sh', [ - '-c', - 'git log "$prevTag"..HEAD --format="%H %ae" --no-merges | sort -u -k2,2', + // Step 1: Get one commit SHA per unique author email. + // Use array args to avoid shell injection via prevTag, then deduplicate + // in Dart instead of piping through `sort -u`. + final gitResult = Process.runSync('git', [ + 'log', + '$prevTag..HEAD', + '--format=%H %ae', + '--no-merges', ], workingDirectory: repoRoot); if (gitResult.exitCode != 0) { @@ -109,7 +113,13 @@ abstract final class ReleaseUtils { return []; } - final lines = (gitResult.stdout as String).trim().split('\n').where((l) => l.isNotEmpty); + // Deduplicate by email (replaces the shell `sort -u -k2,2` pipe). + final seenEmails = {}; + final lines = (gitResult.stdout as String).trim().split('\n').where((l) => l.isNotEmpty).where((l) { + final parts = l.split(' '); + if (parts.length < 2) return false; + return seenEmails.add(parts[1]); // returns false if already seen + }); final contributors = >[]; final seenLogins = {}; @@ -140,8 +150,8 @@ abstract final class ReleaseUtils { contributors.add({'username': login}); } } - } catch (_) { - // API call failed for this SHA, skip + } catch (e) { + Logger.warn('GitHub API call failed for commit $sha: $e'); } } diff --git a/lib/src/cli/utils/sub_package_utils.dart b/lib/src/cli/utils/sub_package_utils.dart index 00e0ece..3de910e 100644 --- a/lib/src/cli/utils/sub_package_utils.dart +++ b/lib/src/cli/utils/sub_package_utils.dart @@ -4,7 +4,6 @@ import 'package:yaml/yaml.dart'; import 'package:yaml_edit/yaml_edit.dart'; import 'logger.dart'; -import 'process_runner.dart'; import 'workflow_generator.dart'; /// Utilities for loading and working with sub-packages defined in @@ -87,12 +86,19 @@ abstract final class SubPackageUtils { buffer.writeln('### Changes in `$name` (`$path/`)'); buffer.writeln(); - // Per-package commit log - final commitLog = CiProcessRunner.runSync( - 'git log $logRange --oneline --no-merges -- $path', - repoRoot, - verbose: verbose, - ); + // Per-package commit log — use Process.runSync with array args to + // avoid shell injection via path or tag values from config.json. + final logArgs = [ + 'log', + ...logRange == 'HEAD' ? ['HEAD'] : [logRange], + '--oneline', + '--no-merges', + '--', + path, + ]; + if (verbose) Logger.info('[CMD] git ${logArgs.join(' ')}'); + final commitLogResult = Process.runSync('git', logArgs, workingDirectory: repoRoot); + final commitLog = (commitLogResult.stdout as String).trim(); if (commitLog.isNotEmpty) { buffer.writeln('Commits:'); buffer.writeln('```'); @@ -105,8 +111,11 @@ abstract final class SubPackageUtils { } buffer.writeln(); - // Per-package diff stat - final diffStat = CiProcessRunner.runSync('git diff --stat $diffRange -- $path', repoRoot, verbose: verbose); + // Per-package diff stat — safe array-based args, no shell interpolation. + final diffArgs = ['diff', '--stat', diffRange, '--', path]; + if (verbose) Logger.info('[CMD] git ${diffArgs.join(' ')}'); + final diffStatResult = Process.runSync('git', diffArgs, workingDirectory: repoRoot); + final diffStat = (diffStatResult.stdout as String).trim(); if (diffStat.isNotEmpty) { buffer.writeln('Diff stat:'); buffer.writeln('```'); @@ -358,19 +367,17 @@ Rules: if (doc.containsKey('resolution')) { try { editor.remove(['resolution']); - if (verbose) { - Logger.info('Stripped resolution: workspace from ${pkg['name']}'); - } - } on Exception catch (_) {} + Logger.info('Stripped resolution: workspace from ${pkg['name']}'); + } on Exception catch (e) { + Logger.warn('Could not strip resolution from ${pkg['name']}: $e'); + } } final updated = editor.toString(); if (updated != original) { + Logger.info('Updating ${pkg['path']}/pubspec.yaml ($conversions sibling dep conversion(s))'); pubspecFile.writeAsStringSync(updated); totalConversions += conversions; - if (verbose) { - Logger.info('Converted $conversions sibling dep(s) in ${pkg['name']}'); - } } } diff --git a/lib/src/triage/utils/json_schemas.dart b/lib/src/triage/utils/json_schemas.dart index 9fd6a85..b234adf 100644 --- a/lib/src/triage/utils/json_schemas.dart +++ b/lib/src/triage/utils/json_schemas.dart @@ -56,14 +56,18 @@ ValidationResult validateInvestigationResult(String path) { /// Writes a JSON object to a file with pretty formatting. void writeJson(String path, Map data) { - File(path).writeAsStringSync('${const JsonEncoder.withIndent(' ').convert(data)}\n'); + final file = File(path); + final label = file.uri.pathSegments.last; + print('[triage] Writing $label (${data.length} keys)'); + file.writeAsStringSync('${const JsonEncoder.withIndent(' ').convert(data)}\n'); } /// Reads and parses a JSON file, returning null on error. Map? readJson(String path) { try { return jsonDecode(File(path).readAsStringSync()) as Map; - } catch (_) { + } catch (e) { + print('[triage] Could not read JSON from $path: $e'); return null; } } diff --git a/templates/github/workflows/ci.skeleton.yaml b/templates/github/workflows/ci.skeleton.yaml index 7aabc78..93717d9 100644 --- a/templates/github/workflows/ci.skeleton.yaml +++ b/templates/github/workflows/ci.skeleton.yaml @@ -67,7 +67,7 @@ jobs: if ! git diff --quiet; then git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A + git add lib/ git commit -m "bot(format): apply dart format --line-length <%line_length%> [skip ci]" if git push; then echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"