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
3 changes: 3 additions & 0 deletions lib/src/cli/commands/create_release_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ class CreateReleaseCommand extends Command<void> {
}

// 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',
Expand All @@ -259,6 +260,7 @@ class CreateReleaseCommand extends Command<void> {
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()) {
Expand Down Expand Up @@ -292,6 +294,7 @@ class CreateReleaseCommand extends Command<void> {
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'],
Expand Down
2 changes: 2 additions & 0 deletions lib/src/cli/manage_cicd.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2016,6 +2016,7 @@ Future<void> _runCreateRelease(String repoRoot, List<String> 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);

Comment on lines +2019 to 2022
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This release flow uses _exec() (which prints args.join(" ") in verbose mode) elsewhere in the same function to run git remote set-url origin https://x-access-token:...@github.com/.... Without redaction in _exec(), enabling _verbose will still leak the token even though CiProcessRunner now redacts. Add a redaction step to _exec()'s verbose command log and its stderr logging (similar to CiProcessRunner._redact()).

Copilot uses AI. Check for mistakes.
Expand All @@ -2033,6 +2034,7 @@ Future<void> _runCreateRelease(String repoRoot, List<String> 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()) {
Expand Down
25 changes: 23 additions & 2 deletions lib/src/cli/utils/process_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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');
Expand All @@ -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<String> 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}');
Expand Down
42 changes: 26 additions & 16 deletions lib/src/cli/utils/release_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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');
Expand All @@ -98,18 +98,28 @@ abstract final class ReleaseUtils {
static List<Map<String, String>> 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) {
Logger.warn('Could not get commit authors from git log');
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 = <String>{};
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
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fallback contributor list now contains duplicate entries

Medium Severity

The conversion from sh -c 'git log ... | sort -u -k2,2' to Process.runSync('git', [...]) with Dart-side deduplication introduces a subtle regression. The Dart dedup only applies to the lazy lines iterable via seenEmails, but the fallback path (when contributors.isEmpty) re-reads gitResult.stdout directly — which is no longer pre-deduplicated by sort -u. A contributor with multiple commits will now appear multiple times in the fallback contributor list.

Fix in Cursor Fix in Web

final contributors = <Map<String, String>>[];
final seenLogins = <String>{};

Expand Down Expand Up @@ -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');
}
}

Expand Down
39 changes: 23 additions & 16 deletions lib/src/cli/utils/sub_package_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = <String>[
'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('```');
Expand All @@ -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('```');
Expand Down Expand Up @@ -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']}');
}
}
}

Expand Down
8 changes: 6 additions & 2 deletions lib/src/triage/utils/json_schemas.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,18 @@ ValidationResult validateInvestigationResult(String path) {

/// Writes a JSON object to a file with pretty formatting.
void writeJson(String path, Map<String, dynamic> 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)');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unconditional print in writeJson pollutes stdout output

Low Severity

The writeJson function now has an unconditional print() call on every invocation. This utility is called from at least 6 locations across triage phases. Unlike the error-path logging added to readJson, this fires on every successful write, producing noise on stdout with no way for callers to suppress it. Using raw print() also bypasses any structured logging, and stdout output could interfere with piped or machine-readable CLI output.

Fix in Cursor Fix in Web

file.writeAsStringSync('${const JsonEncoder.withIndent(' ').convert(data)}\n');
}

/// Reads and parses a JSON file, returning null on error.
Map<String, dynamic>? readJson(String path) {
try {
return jsonDecode(File(path).readAsStringSync()) as Map<String, dynamic>;
} catch (_) {
} catch (e) {
print('[triage] Could not read JSON from $path: $e');
return null;
}
}
Expand Down
2 changes: 1 addition & 1 deletion templates/github/workflows/ci.skeleton.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading