refactor: improve repo structure
This commit is contained in:
parent
2bdf75d2ad
commit
19af0bbd1b
|
|
@ -3,3 +3,5 @@
|
||||||
- read `README.md` for general information
|
- read `README.md` for general information
|
||||||
run `pnpx skills experimental_install`, `lefthook install` before development at first time.
|
run `pnpx skills experimental_install`, `lefthook install` before development at first time.
|
||||||
- For any new or modified Dart test under `test/`, use the repo-local skill `dart-gherkin-tests`.
|
- For any new or modified Dart test under `test/`, use the repo-local skill `dart-gherkin-tests`.
|
||||||
|
- For file more than 500 lines, consider to split it into multiple files for better maintainability.
|
||||||
|
- Keep refactoring e.g. `SOLID` principles
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
export 'src/comment_templates.dart';
|
export 'src/config/comment_templates.dart';
|
||||||
export 'src/config.dart';
|
export 'src/config/config.dart';
|
||||||
export 'src/database.dart';
|
export 'src/core/database.dart';
|
||||||
export 'src/app_logger.dart';
|
export 'src/config/app_logger.dart';
|
||||||
export 'src/app_environment.dart';
|
export 'src/config/app_environment.dart';
|
||||||
export 'src/issue_event_source/base.dart';
|
export 'src/issue_tracker/issue_event_source/base.dart';
|
||||||
export 'src/issue_tracker_client.dart';
|
export 'src/issue_tracker/issue_tracker_client.dart';
|
||||||
export 'src/issue_assistant_app.dart';
|
export 'src/core/issue_assistant_app.dart';
|
||||||
export 'src/notifier.dart';
|
export 'src/notifier/notifier.dart';
|
||||||
export 'src/scm_client.dart';
|
export 'src/workspace/scm_client.dart';
|
||||||
export 'src/workspace_manager.dart';
|
export 'src/workspace/workspace_manager.dart';
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'config.dart';
|
import '../config/config.dart';
|
||||||
import 'issue_tracker_client.dart';
|
import '../issue_tracker/issue_tracker_client.dart';
|
||||||
import 'process_launcher.dart';
|
import 'process_launcher.dart';
|
||||||
|
|
||||||
class CliResponderRunner {
|
class CliResponderRunner {
|
||||||
|
|
@ -3,17 +3,17 @@ import 'dart:convert';
|
||||||
|
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
|
|
||||||
import 'app_logger.dart';
|
import '../config/app_logger.dart';
|
||||||
import 'cli_responder.dart';
|
import 'cli_responder.dart';
|
||||||
import 'config.dart';
|
import '../config/config.dart';
|
||||||
import 'database.dart';
|
import 'database.dart';
|
||||||
import 'issue_event_source/base.dart';
|
import '../issue_tracker/issue_event_source/base.dart';
|
||||||
import 'issue_event_source/gitea_gosmee.dart';
|
import '../issue_tracker/issue_event_source/gitea_gosmee.dart';
|
||||||
import 'issue_event_source/github_gosmee.dart';
|
import '../issue_tracker/issue_event_source/github_gosmee.dart';
|
||||||
import 'issue_event_source/github_webhook_forward.dart';
|
import '../issue_tracker/issue_event_source/github_webhook_forward.dart';
|
||||||
import 'issue_tracker_client.dart';
|
import '../issue_tracker/issue_tracker_client.dart';
|
||||||
import 'notifier.dart';
|
import '../notifier/notifier.dart';
|
||||||
import 'workspace_manager.dart';
|
import '../workspace/workspace_manager.dart';
|
||||||
|
|
||||||
class IssueAssistantApp {
|
class IssueAssistantApp {
|
||||||
static final AppLogger _logger = AppLogger('code_work_spawner.app');
|
static final AppLogger _logger = AppLogger('code_work_spawner.app');
|
||||||
|
|
@ -333,7 +333,7 @@ class IssueAssistantApp {
|
||||||
stdout: rawStdout,
|
stdout: rawStdout,
|
||||||
stderr: rawStderr,
|
stderr: rawStderr,
|
||||||
);
|
);
|
||||||
_log(
|
_logError(
|
||||||
'responder=${responder.id} issue=${project.key}#${issue.number} '
|
'responder=${responder.id} issue=${project.key}#${issue.number} '
|
||||||
'failed error=$error',
|
'failed error=$error',
|
||||||
);
|
);
|
||||||
|
|
@ -4,9 +4,9 @@ import 'dart:io';
|
||||||
import 'package:github/github.dart';
|
import 'package:github/github.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
import '../app_environment.dart';
|
import '../config/app_environment.dart';
|
||||||
import '../config_schema.dart';
|
import '../config/config_schema.dart';
|
||||||
import '../process_launcher.dart';
|
import '../core/process_launcher.dart';
|
||||||
import 'models.dart';
|
import 'models.dart';
|
||||||
|
|
||||||
class IssueTrackerClient {
|
class IssueTrackerClient {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import '../app_logger.dart';
|
import '../../config/app_logger.dart';
|
||||||
import '../config.dart';
|
import '../../config/config.dart';
|
||||||
import '../database.dart';
|
import '../../core/database.dart';
|
||||||
import '../issue_tracker_client.dart';
|
import '../client.dart';
|
||||||
|
import '../models.dart';
|
||||||
|
|
||||||
typedef IssueEventBatchHandler = Future<void> Function(IssueEventBatch batch);
|
typedef IssueEventBatchHandler = Future<void> Function(IssueEventBatch batch);
|
||||||
|
|
||||||
|
|
@ -4,10 +4,11 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:github/github.dart';
|
import 'package:github/github.dart';
|
||||||
|
|
||||||
import '../app_logger.dart';
|
import '../../config/app_logger.dart';
|
||||||
import '../config.dart';
|
import '../../config/config.dart';
|
||||||
import '../issue_tracker_client.dart';
|
import '../client.dart';
|
||||||
import '../process_launcher.dart';
|
import '../models.dart';
|
||||||
|
import '../../core/process_launcher.dart';
|
||||||
import 'base.dart';
|
import 'base.dart';
|
||||||
|
|
||||||
class GiteaGosmeeIssueEventSource implements IssueEventSource {
|
class GiteaGosmeeIssueEventSource implements IssueEventSource {
|
||||||
|
|
@ -4,10 +4,11 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:github/github.dart';
|
import 'package:github/github.dart';
|
||||||
|
|
||||||
import '../app_logger.dart';
|
import '../../config/app_logger.dart';
|
||||||
import '../config.dart';
|
import '../../config/config.dart';
|
||||||
import '../issue_tracker_client.dart';
|
import '../client.dart';
|
||||||
import '../process_launcher.dart';
|
import '../models.dart';
|
||||||
|
import '../../core/process_launcher.dart';
|
||||||
import 'base.dart';
|
import 'base.dart';
|
||||||
|
|
||||||
class GitHubGosmeeIssueEventSource implements IssueEventSource {
|
class GitHubGosmeeIssueEventSource implements IssueEventSource {
|
||||||
|
|
@ -2,10 +2,11 @@ import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import '../app_logger.dart';
|
import '../../config/app_logger.dart';
|
||||||
import '../config.dart';
|
import '../../config/config.dart';
|
||||||
import '../issue_tracker_client.dart';
|
import '../client.dart';
|
||||||
import '../process_launcher.dart';
|
import '../models.dart';
|
||||||
|
import '../../core/process_launcher.dart';
|
||||||
import 'base.dart';
|
import 'base.dart';
|
||||||
|
|
||||||
class GitHubWebhookForwardIssueEventSource implements IssueEventSource {
|
class GitHubWebhookForwardIssueEventSource implements IssueEventSource {
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export 'client.dart';
|
||||||
|
export 'models.dart';
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export 'issue_tracker/client.dart';
|
|
||||||
export 'issue_tracker/models.dart';
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'config.dart';
|
import '../config/config.dart';
|
||||||
|
|
||||||
typedef NotificationLogger = void Function(String message);
|
typedef NotificationLogger = void Function(String message);
|
||||||
typedef DesktopNotificationCommandRunner =
|
typedef DesktopNotificationCommandRunner =
|
||||||
|
|
@ -3,8 +3,8 @@ import 'dart:io';
|
||||||
import 'package:git/git.dart';
|
import 'package:git/git.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
import 'app_environment.dart';
|
import '../config/app_environment.dart';
|
||||||
import 'config.dart';
|
import '../config/config.dart';
|
||||||
|
|
||||||
abstract interface class ScmClient {
|
abstract interface class ScmClient {
|
||||||
Future<String> createEphemeralWorkspace({
|
Future<String> createEphemeralWorkspace({
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import 'config.dart';
|
import '../config/config.dart';
|
||||||
import 'scm_client.dart';
|
import 'scm_client.dart';
|
||||||
|
|
||||||
class WorkspaceManager {
|
class WorkspaceManager {
|
||||||
|
|
@ -349,7 +349,7 @@
|
||||||
"eval-driven-dev": {
|
"eval-driven-dev": {
|
||||||
"source": "github/awesome-copilot",
|
"source": "github/awesome-copilot",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"computedHash": "0b2cf0b8a57cf332eecb36ca00c6bb0636a3b0ecfc1f79f28b1fd4bab7f8d4fb"
|
"computedHash": "1b5d8a5edb5c3540cd260e9ac71394077f814151b71fc343afb6ffd7e15c21b9"
|
||||||
},
|
},
|
||||||
"excalidraw-diagram-generator": {
|
"excalidraw-diagram-generator": {
|
||||||
"source": "github/awesome-copilot",
|
"source": "github/awesome-copilot",
|
||||||
|
|
@ -944,7 +944,7 @@
|
||||||
"worktrunk": {
|
"worktrunk": {
|
||||||
"source": "max-sixty/worktrunk",
|
"source": "max-sixty/worktrunk",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"computedHash": "d480468e16c7de39a121e6e760788357bb178495f8912b05f05f55977468d72e"
|
"computedHash": "e95e53e4ef26baddf9efb6b6c4e08276384edba9d7e9269d61a11f2c6871c6d5"
|
||||||
},
|
},
|
||||||
"write-coding-standards-from-file": {
|
"write-coding-standards-from-file": {
|
||||||
"source": "github/awesome-copilot",
|
"source": "github/awesome-copilot",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import 'package:code_work_spawner/code_work_spawner.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import '../test_support.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
/// ```gherkin
|
/// ```gherkin
|
||||||
/// Feature: Init-config CLI
|
/// Feature: Init-config CLI
|
||||||
|
|
@ -21,21 +23,16 @@ void main() {
|
||||||
/// ```
|
/// ```
|
||||||
test('Show init-config in top-level help', () async {
|
test('Show init-config in top-level help', () async {
|
||||||
// Given the CLI is executed with the top-level help flag.
|
// Given the CLI is executed with the top-level help flag.
|
||||||
final result = await Process.run(Platform.resolvedExecutable, [
|
final result = await runCli(['--help']);
|
||||||
'run',
|
|
||||||
'bin/code_work_spawner.dart',
|
|
||||||
'--help',
|
|
||||||
], workingDirectory: Directory.current.path);
|
|
||||||
|
|
||||||
// When the process completes successfully.
|
// When the process completes successfully.
|
||||||
expect(result.exitCode, 0, reason: '${result.stderr}');
|
expect(result.exitCode, 0, reason: result.stderr);
|
||||||
|
|
||||||
// Then stdout lists the init-config command in the available commands section.
|
// Then stdout lists the init-config command in the available commands section.
|
||||||
final stdoutText = result.stdout as String;
|
expect(result.stdout, contains('Available commands:'));
|
||||||
expect(stdoutText, contains('Available commands:'));
|
expect(result.stdout, contains('init-config'));
|
||||||
expect(stdoutText, contains('init-config'));
|
expect(result.stdout, contains('--glab-command'));
|
||||||
expect(stdoutText, contains('--glab-command'));
|
expect(result.stdout, contains('--gosmee-command'));
|
||||||
expect(stdoutText, contains('--gosmee-command'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// ```gherkin
|
/// ```gherkin
|
||||||
|
|
@ -49,29 +46,27 @@ void main() {
|
||||||
// Given the CLI is executed with the init-config command.
|
// Given the CLI is executed with the init-config command.
|
||||||
final sandbox = await Directory.systemTemp.createTemp('cws-cli-stdout-');
|
final sandbox = await Directory.systemTemp.createTemp('cws-cli-stdout-');
|
||||||
final configFile = File(p.join(sandbox.path, 'agent-orchestrator.yaml'));
|
final configFile = File(p.join(sandbox.path, 'agent-orchestrator.yaml'));
|
||||||
final result = await Process.run(Platform.resolvedExecutable, [
|
final result = await runCli(['init-config']);
|
||||||
'run',
|
|
||||||
'bin/code_work_spawner.dart',
|
|
||||||
'init-config',
|
|
||||||
], workingDirectory: Directory.current.path);
|
|
||||||
|
|
||||||
// When the process completes successfully.
|
// When the process completes successfully.
|
||||||
expect(result.exitCode, 0, reason: '${result.stderr}');
|
expect(result.exitCode, 0, reason: result.stderr);
|
||||||
|
|
||||||
// Then stdout contains the starter config content.
|
// Then stdout contains the starter config content.
|
||||||
final stdoutText = result.stdout as String;
|
expect(result.stdout, contains('dataDir:'));
|
||||||
expect(stdoutText, contains('dataDir:'));
|
expect(result.stdout, contains('# responders:'));
|
||||||
expect(stdoutText, contains('# responders:'));
|
expect(result.stdout, contains('# notifications:'));
|
||||||
expect(stdoutText, contains('# notifications:'));
|
expect(
|
||||||
expect(stdoutText, contains(r'# webhookUrl: ${CWS_DISCORD_WEBHOOK}'));
|
result.stdout,
|
||||||
expect(stdoutText, contains('# - type: desktop'));
|
contains(r'# webhookUrl: ${CWS_DISCORD_WEBHOOK}'),
|
||||||
expect(stdoutText, contains('issueTracker:'));
|
);
|
||||||
expect(stdoutText, contains('provider: github'));
|
expect(result.stdout, contains('# - type: desktop'));
|
||||||
expect(stdoutText, contains('scm:'));
|
expect(result.stdout, contains('issueTracker:'));
|
||||||
expect(stdoutText, contains('projects:'));
|
expect(result.stdout, contains('provider: github'));
|
||||||
|
expect(result.stdout, contains('scm:'));
|
||||||
|
expect(result.stdout, contains('projects:'));
|
||||||
|
|
||||||
// And the printed config is valid when written to disk and loaded.
|
// And the printed config is valid when written to disk and loaded.
|
||||||
await configFile.writeAsString(stdoutText);
|
await configFile.writeAsString(result.stdout);
|
||||||
final config = await AppConfig.load(configFile.path);
|
final config = await AppConfig.load(configFile.path);
|
||||||
expect(config.projects['sample'], isNotNull);
|
expect(config.projects['sample'], isNotNull);
|
||||||
});
|
});
|
||||||
|
|
@ -90,31 +85,27 @@ void main() {
|
||||||
await outputFile.writeAsString('original');
|
await outputFile.writeAsString('original');
|
||||||
|
|
||||||
// When the CLI writes without the force flag and then with the force flag.
|
// When the CLI writes without the force flag and then with the force flag.
|
||||||
final withoutForce = await Process.run(Platform.resolvedExecutable, [
|
final withoutForce = await runCli([
|
||||||
'run',
|
|
||||||
'bin/code_work_spawner.dart',
|
|
||||||
'init-config',
|
'init-config',
|
||||||
'--output',
|
'--output',
|
||||||
outputFile.path,
|
outputFile.path,
|
||||||
], workingDirectory: Directory.current.path);
|
]);
|
||||||
final withForce = await Process.run(Platform.resolvedExecutable, [
|
final withForce = await runCli([
|
||||||
'run',
|
|
||||||
'bin/code_work_spawner.dart',
|
|
||||||
'init-config',
|
'init-config',
|
||||||
'--output',
|
'--output',
|
||||||
outputFile.path,
|
outputFile.path,
|
||||||
'--force',
|
'--force',
|
||||||
], workingDirectory: Directory.current.path);
|
]);
|
||||||
|
|
||||||
// Then the first run fails with an overwrite error.
|
// Then the first run fails with an overwrite error.
|
||||||
expect(withoutForce.exitCode, 2);
|
expect(withoutForce.exitCode, 2);
|
||||||
expect(
|
expect(
|
||||||
withoutForce.stderr as String,
|
withoutForce.stderr,
|
||||||
contains('Refusing to overwrite existing file'),
|
contains('Refusing to overwrite existing file'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// And the forced run replaces the file with a valid starter config.
|
// And the forced run replaces the file with a valid starter config.
|
||||||
expect(withForce.exitCode, 0, reason: '${withForce.stderr}');
|
expect(withForce.exitCode, 0, reason: withForce.stderr);
|
||||||
final contents = await outputFile.readAsString();
|
final contents = await outputFile.readAsString();
|
||||||
expect(contents, contains('defaults:'));
|
expect(contents, contains('defaults:'));
|
||||||
expect(contents, contains('issueTracker:'));
|
expect(contents, contains('issueTracker:'));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../../bin/code_work_spawner.dart' as _cliEntry;
|
||||||
|
|
||||||
|
/// Result of a [runCli] invocation.
|
||||||
|
typedef CliResult = ({int exitCode, String stdout, String stderr});
|
||||||
|
|
||||||
|
/// Runs the CLI [main] in-process, capturing stdout, stderr, and [exitCode].
|
||||||
|
///
|
||||||
|
/// Uses [IOOverrides.runZoned] so every `dart:io` write inside the CLI lands
|
||||||
|
/// in the capturing sinks rather than the real terminal. The global
|
||||||
|
/// [exitCode] is reset to 0 both before and after the call so that
|
||||||
|
/// individual tests do not pollute each other.
|
||||||
|
Future<CliResult> runCli(List<String> args) async {
|
||||||
|
final out = _CapturingStdout();
|
||||||
|
final err = _CapturingStdout();
|
||||||
|
exitCode = 0;
|
||||||
|
try {
|
||||||
|
await IOOverrides.runZoned<Future<void>>(
|
||||||
|
() => _cliEntry.main(args),
|
||||||
|
stdout: () => out,
|
||||||
|
stderr: () => err,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
// Errors surface through exitCode / captured stderr, not as exceptions.
|
||||||
|
}
|
||||||
|
final result = (exitCode: exitCode, stdout: out.text, stderr: err.text);
|
||||||
|
exitCode = 0;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [Stdout] implementation that captures all written text into a buffer.
|
||||||
|
class _CapturingStdout implements Stdout {
|
||||||
|
final StringBuffer _buffer = StringBuffer();
|
||||||
|
|
||||||
|
String get text => _buffer.toString();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Encoding encoding = utf8;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get hasTerminal => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get terminalColumns => throw UnsupportedError('no terminal');
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get terminalLines => throw UnsupportedError('no terminal');
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get supportsAnsiEscapes => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
IOSink get nonBlocking => this;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String lineTerminator = '\n';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void write(Object? object) => _buffer.write(object);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void writeln([Object? object = '']) => _buffer.writeln(object);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void writeAll(Iterable<dynamic> objects, [String separator = '']) =>
|
||||||
|
_buffer.writeAll(objects, separator);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void writeCharCode(int charCode) => _buffer.writeCharCode(charCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void add(List<int> data) => _buffer.write(utf8.decode(data));
|
||||||
|
|
||||||
|
@override
|
||||||
|
void addError(Object error, [StackTrace? stackTrace]) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> addStream(Stream<List<int>> stream) async {
|
||||||
|
await for (final chunk in stream) {
|
||||||
|
_buffer.write(utf8.decode(chunk));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> flush() => Future.value();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() => Future.value();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> get done => Future.value();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,522 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'script_utils.dart';
|
||||||
|
|
||||||
|
Future<void> writeFakeGhScript({
|
||||||
|
required File ghScript,
|
||||||
|
required List<Map<String, Object?>> issueListResponse,
|
||||||
|
required Map<int, List<Map<String, Object?>>> commentResponses,
|
||||||
|
Map<String, List<Map<String, Object?>>>? issueListResponsesByRepo,
|
||||||
|
Map<String, Map<int, List<Map<String, Object?>>>>? commentResponsesByRepo,
|
||||||
|
File? ghLog,
|
||||||
|
int? postCommentIssueNumber,
|
||||||
|
Set<int>? postCommentIssueNumbers,
|
||||||
|
Map<String, Set<int>>? postCommentIssueNumbersByRepo,
|
||||||
|
File? postedBodyFile,
|
||||||
|
String? viewerLogin,
|
||||||
|
bool failViewerLookup = false,
|
||||||
|
}) async {
|
||||||
|
final normalizedIssueListResponsesByRepo =
|
||||||
|
issueListResponsesByRepo ??
|
||||||
|
<String, List<Map<String, Object?>>>{'owner/sample': issueListResponse};
|
||||||
|
final normalizedCommentResponsesByRepo =
|
||||||
|
commentResponsesByRepo ??
|
||||||
|
<String, Map<int, List<Map<String, Object?>>>>{
|
||||||
|
'owner/sample': commentResponses,
|
||||||
|
};
|
||||||
|
final normalizedPostCommentIssueNumbersByRepo =
|
||||||
|
postCommentIssueNumbersByRepo ??
|
||||||
|
<String, Set<int>>{
|
||||||
|
'owner/sample': {
|
||||||
|
...?postCommentIssueNumbers,
|
||||||
|
...?postCommentIssueNumber == null ? null : {postCommentIssueNumber},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
final searchItems =
|
||||||
|
normalizedIssueListResponsesByRepo.entries
|
||||||
|
.expand(
|
||||||
|
(entry) => entry.value.map(
|
||||||
|
(issue) => <String, Object?>{
|
||||||
|
...issue,
|
||||||
|
'repository_url': 'https://api.github.com/repos/${entry.key}',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(growable: false)
|
||||||
|
..sort((left, right) {
|
||||||
|
final leftUpdatedAt = left['updated_at'] as String? ?? '';
|
||||||
|
final rightUpdatedAt = right['updated_at'] as String? ?? '';
|
||||||
|
return leftUpdatedAt.compareTo(rightUpdatedAt);
|
||||||
|
});
|
||||||
|
final openSearchItems = searchItems
|
||||||
|
.where((item) => item['state'] == 'open')
|
||||||
|
.toList(growable: false);
|
||||||
|
final allSearchResponse = jsonEncode({
|
||||||
|
'total_count': searchItems.length,
|
||||||
|
'incomplete_results': false,
|
||||||
|
'items': searchItems,
|
||||||
|
});
|
||||||
|
final openSearchResponse = jsonEncode({
|
||||||
|
'total_count': openSearchItems.length,
|
||||||
|
'incomplete_results': false,
|
||||||
|
'items': openSearchItems,
|
||||||
|
});
|
||||||
|
|
||||||
|
final buffer = StringBuffer()
|
||||||
|
..writeln('#!/usr/bin/env bash')
|
||||||
|
..writeln('set -euo pipefail');
|
||||||
|
|
||||||
|
if (ghLog != null) {
|
||||||
|
buffer.writeln(
|
||||||
|
r'''printf '%s\n' "$*" >> '''
|
||||||
|
'"${bashScriptPath(ghLog.path)}"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.writeln(r'''if [[ "$1" == "api" && "$2" == "user" ]]; then''');
|
||||||
|
if (failViewerLookup) {
|
||||||
|
buffer
|
||||||
|
..writeln(r''' echo "viewer lookup failed" >&2''')
|
||||||
|
..writeln(' exit 1')
|
||||||
|
..writeln('fi');
|
||||||
|
} else {
|
||||||
|
buffer
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(jsonEncode({'login': viewerLogin ?? 'test-user'}))
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(r'''if [[ "$1" == "api" && "$4" == "search/issues" ]]; then''')
|
||||||
|
..writeln(r''' query=""''')
|
||||||
|
..writeln(r''' for arg in "$@"; do''')
|
||||||
|
..writeln(r''' if [[ "$arg" == q=* ]]; then''')
|
||||||
|
..writeln(r''' query="${arg#q=}"''')
|
||||||
|
..writeln(r''' fi''')
|
||||||
|
..writeln(r''' done''')
|
||||||
|
..writeln(r''' if [[ "$query" == *"state:open"* ]]; then''')
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(openSearchResponse)
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(r''' else''')
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(allSearchResponse)
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(r''' fi''')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
|
||||||
|
for (final entry in normalizedIssueListResponsesByRepo.entries) {
|
||||||
|
final repo = entry.key;
|
||||||
|
buffer
|
||||||
|
..writeln(
|
||||||
|
'if [[ "\$1" == "api" && "\$4" == repos/$repo/issues\\?* ]]; then',
|
||||||
|
)
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(jsonEncode(entry.value))
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final repoEntry in normalizedCommentResponsesByRepo.entries) {
|
||||||
|
final repo = repoEntry.key;
|
||||||
|
for (final entry in repoEntry.value.entries) {
|
||||||
|
buffer
|
||||||
|
..writeln(
|
||||||
|
'if [[ "\$1" == "api" && "\$4" == '
|
||||||
|
'"repos/$repo/issues/${entry.key}/comments?per_page=100" ]]; then',
|
||||||
|
)
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(jsonEncode(entry.value))
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final repoEntry in normalizedPostCommentIssueNumbersByRepo.entries) {
|
||||||
|
final repo = repoEntry.key;
|
||||||
|
for (final issueNumber in repoEntry.value) {
|
||||||
|
buffer
|
||||||
|
..writeln(
|
||||||
|
'if [[ "\$1" == "api" && "\$4" == '
|
||||||
|
'"repos/$repo/issues/$issueNumber/comments" ]]; then',
|
||||||
|
)
|
||||||
|
..writeln(r''' for arg in "$@"; do''')
|
||||||
|
..writeln(r''' :''');
|
||||||
|
if (postedBodyFile != null) {
|
||||||
|
buffer
|
||||||
|
..writeln(r''' if [[ "$arg" == body=* ]]; then''')
|
||||||
|
..writeln(
|
||||||
|
r''' printf '%s' "${arg#body=}" > '''
|
||||||
|
'"${bashScriptPath(postedBodyFile.path)}"',
|
||||||
|
)
|
||||||
|
..writeln(r''' fi''');
|
||||||
|
}
|
||||||
|
buffer
|
||||||
|
..writeln(r''' done''')
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(
|
||||||
|
jsonEncode({
|
||||||
|
'id': 700,
|
||||||
|
'html_url': 'https://example.test/comment/700',
|
||||||
|
'body': 'posted',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(r'''echo "unexpected gh args: $*" >&2''')
|
||||||
|
..writeln('exit 1');
|
||||||
|
|
||||||
|
await ghScript.writeAsString(buffer.toString());
|
||||||
|
await makeScriptExecutable(ghScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> writeFlakySearchGhScript({
|
||||||
|
required File ghScript,
|
||||||
|
required File failureStateFile,
|
||||||
|
required List<Map<String, Object?>> issueListResponse,
|
||||||
|
required Map<int, List<Map<String, Object?>>> commentResponses,
|
||||||
|
File? ghLog,
|
||||||
|
}) async {
|
||||||
|
final searchItems = issueListResponse
|
||||||
|
.map(
|
||||||
|
(issue) => <String, Object?>{
|
||||||
|
...issue,
|
||||||
|
'repository_url': 'https://api.github.com/repos/owner/sample',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toList(growable: false);
|
||||||
|
final searchResponse = jsonEncode({
|
||||||
|
'total_count': searchItems.length,
|
||||||
|
'incomplete_results': false,
|
||||||
|
'items': searchItems,
|
||||||
|
});
|
||||||
|
|
||||||
|
final buffer = StringBuffer()
|
||||||
|
..writeln('#!/usr/bin/env bash')
|
||||||
|
..writeln('set -euo pipefail');
|
||||||
|
|
||||||
|
if (ghLog != null) {
|
||||||
|
buffer.writeln(
|
||||||
|
r'''printf '%s\n' "$*" >> '''
|
||||||
|
'"${bashScriptPath(ghLog.path)}"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(r'''if [[ "$1" == "api" && "$2" == "user" ]]; then''')
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(jsonEncode({'login': 'test-user'}))
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(r'''if [[ "$1" == "api" && "$4" == "search/issues" ]]; then''')
|
||||||
|
..writeln(
|
||||||
|
' if [[ ! -f "${bashScriptPath(failureStateFile.path)}" ]]; then',
|
||||||
|
)
|
||||||
|
..writeln(' touch "${bashScriptPath(failureStateFile.path)}"')
|
||||||
|
..writeln(r''' echo "error connecting to api.github.com" >&2''')
|
||||||
|
..writeln(
|
||||||
|
r''' echo "check your internet connection or https://githubstatus.com" >&2''',
|
||||||
|
)
|
||||||
|
..writeln(' exit 1')
|
||||||
|
..writeln(r''' fi''')
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(searchResponse)
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
|
||||||
|
for (final entry in commentResponses.entries) {
|
||||||
|
buffer
|
||||||
|
..writeln(
|
||||||
|
'if [[ "\$1" == "api" && "\$4" == '
|
||||||
|
'"repos/owner/sample/issues/${entry.key}/comments?per_page=100" ]]; then',
|
||||||
|
)
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(jsonEncode(entry.value))
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(r'''echo "unexpected gh args: $*" >&2''')
|
||||||
|
..writeln('exit 1');
|
||||||
|
|
||||||
|
await ghScript.writeAsString(buffer.toString());
|
||||||
|
await makeScriptExecutable(ghScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> writeWebhookForwardGhScript({
|
||||||
|
required File ghScript,
|
||||||
|
required String repo,
|
||||||
|
required Map<String, Object?> issue,
|
||||||
|
required List<Map<String, Object?>> comments,
|
||||||
|
List<Map<String, Object?>> issueListResponse = const [],
|
||||||
|
File? ghLog,
|
||||||
|
File? postedBodyFile,
|
||||||
|
String? viewerLogin,
|
||||||
|
}) async {
|
||||||
|
final issueNumber = issue['number'] as int;
|
||||||
|
final payload = {
|
||||||
|
'action': 'created',
|
||||||
|
'issue': issue,
|
||||||
|
'comment': comments.last,
|
||||||
|
'repository': {'full_name': repo},
|
||||||
|
};
|
||||||
|
final searchItems = issueListResponse
|
||||||
|
.map(
|
||||||
|
(searchIssue) => <String, Object?>{
|
||||||
|
...searchIssue,
|
||||||
|
'repository_url': 'https://api.github.com/repos/$repo',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toList(growable: false);
|
||||||
|
final searchResponse = jsonEncode({
|
||||||
|
'total_count': searchItems.length,
|
||||||
|
'incomplete_results': false,
|
||||||
|
'items': searchItems,
|
||||||
|
});
|
||||||
|
|
||||||
|
final buffer = StringBuffer()
|
||||||
|
..writeln('#!/usr/bin/env bash')
|
||||||
|
..writeln('set -euo pipefail');
|
||||||
|
|
||||||
|
if (ghLog != null) {
|
||||||
|
buffer.writeln(
|
||||||
|
r'''printf '%s\n' "$*" >> '''
|
||||||
|
'"${bashScriptPath(ghLog.path)}"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.writeln(r'''if [[ "$1" == "api" && "$2" == "user" ]]; then''');
|
||||||
|
buffer
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(jsonEncode({'login': viewerLogin ?? 'test-user'}))
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(r'''if [[ "$1" == "api" && "$4" == "search/issues" ]]; then''')
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(searchResponse)
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(
|
||||||
|
'if [[ "\$1" == "api" && "\$4" == '
|
||||||
|
'"repos/$repo/issues/$issueNumber/comments?per_page=100" ]]; then',
|
||||||
|
)
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(jsonEncode(comments))
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(
|
||||||
|
'if [[ "\$1" == "api" && "\$4" == '
|
||||||
|
'"repos/$repo/issues/$issueNumber/comments" ]]; then',
|
||||||
|
)
|
||||||
|
..writeln(r''' for arg in "$@"; do''')
|
||||||
|
..writeln(r''' :''');
|
||||||
|
if (postedBodyFile != null) {
|
||||||
|
buffer
|
||||||
|
..writeln(r''' if [[ "$arg" == body=* ]]; then''')
|
||||||
|
..writeln(
|
||||||
|
r''' printf '%s' "${arg#body=}" > '''
|
||||||
|
'"${bashScriptPath(postedBodyFile.path)}"',
|
||||||
|
)
|
||||||
|
..writeln(r''' fi''');
|
||||||
|
}
|
||||||
|
buffer
|
||||||
|
..writeln(r''' done''')
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(
|
||||||
|
jsonEncode({
|
||||||
|
'id': 702,
|
||||||
|
'html_url': 'https://example.test/comment/702',
|
||||||
|
'body': 'posted',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(r'''if [[ "$1" == "webhook" && "$2" == "forward" ]]; then''')
|
||||||
|
..writeln(r''' url=""''')
|
||||||
|
..writeln(r''' for arg in "$@"; do''')
|
||||||
|
..writeln(r''' if [[ "$arg" == --url=* ]]; then''')
|
||||||
|
..writeln(r''' url="${arg#--url=}"''')
|
||||||
|
..writeln(r''' fi''')
|
||||||
|
..writeln(r''' done''')
|
||||||
|
..writeln(r''' if [[ -z "$url" ]]; then''')
|
||||||
|
..writeln(r''' echo "missing --url" >&2''')
|
||||||
|
..writeln(' exit 1')
|
||||||
|
..writeln(r''' fi''')
|
||||||
|
..writeln(r''' curl -sS -X POST \''')
|
||||||
|
..writeln(r''' -H "Content-Type: application/json" \''')
|
||||||
|
..writeln(r''' -H "X-GitHub-Event: issue_comment" \''')
|
||||||
|
..writeln(r''' --data-binary @- "$url" <<'JSON' >/dev/null''')
|
||||||
|
..writeln(jsonEncode(payload))
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(r''' while true; do''')
|
||||||
|
..writeln(r''' sleep 60''')
|
||||||
|
..writeln(r''' done''')
|
||||||
|
..writeln('fi');
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(r'''echo "unexpected gh args: $*" >&2''')
|
||||||
|
..writeln('exit 1');
|
||||||
|
|
||||||
|
await ghScript.writeAsString(buffer.toString());
|
||||||
|
await makeScriptExecutable(ghScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> writeGitHubGosmeeGhScript({
|
||||||
|
required File ghScript,
|
||||||
|
required String repo,
|
||||||
|
required int webhookId,
|
||||||
|
required int issueNumber,
|
||||||
|
required List<Map<String, Object?>> comments,
|
||||||
|
List<Map<String, Object?>> issueListResponse = const [],
|
||||||
|
File? ghLog,
|
||||||
|
File? postedBodyFile,
|
||||||
|
String? viewerLogin,
|
||||||
|
}) async {
|
||||||
|
final searchItems = issueListResponse
|
||||||
|
.map(
|
||||||
|
(issue) => <String, Object?>{
|
||||||
|
...issue,
|
||||||
|
'repository_url': 'https://api.github.com/repos/$repo',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toList(growable: false);
|
||||||
|
final searchResponse = jsonEncode({
|
||||||
|
'total_count': searchItems.length,
|
||||||
|
'incomplete_results': false,
|
||||||
|
'items': searchItems,
|
||||||
|
});
|
||||||
|
final buffer = StringBuffer()
|
||||||
|
..writeln('#!/usr/bin/env bash')
|
||||||
|
..writeln('set -euo pipefail');
|
||||||
|
|
||||||
|
if (ghLog != null) {
|
||||||
|
buffer.writeln(
|
||||||
|
r'''printf '%s\n' "$*" >> '''
|
||||||
|
'"${bashScriptPath(ghLog.path)}"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.writeln(r'''if [[ "$1" == "api" && "$2" == "user" ]]; then''');
|
||||||
|
buffer
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(jsonEncode({'login': viewerLogin ?? 'test-user'}))
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(r'''if [[ "$1" == "api" && "$4" == "search/issues" ]]; then''')
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(searchResponse)
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(
|
||||||
|
'if [[ "\$1" == "api" && "\$4" == '
|
||||||
|
'"repos/$repo/issues/$issueNumber/comments?per_page=100" ]]; then',
|
||||||
|
)
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(jsonEncode(comments))
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln('if [[ "\$1" == "api" && "\$4" == "repos/$repo/hooks" ]]; then')
|
||||||
|
..writeln(r''' active_flag=""''')
|
||||||
|
..writeln(r''' for ((i = 1; i <= $#; i += 1)); do''')
|
||||||
|
..writeln(r''' if [[ "${!i}" == "active=true" ]]; then''')
|
||||||
|
..writeln(r''' prev_index=$((i - 1))''')
|
||||||
|
..writeln(r''' active_flag="${!prev_index}"''')
|
||||||
|
..writeln(r''' break''')
|
||||||
|
..writeln(r''' fi''')
|
||||||
|
..writeln(r''' done''')
|
||||||
|
..writeln(
|
||||||
|
r''' if [[ "$active_flag" != "-F" && "$active_flag" != "--field" ]]; then''',
|
||||||
|
)
|
||||||
|
..writeln(
|
||||||
|
r''' echo "expected active=true to be sent with -F/--field, got: $*" >&2''',
|
||||||
|
)
|
||||||
|
..writeln(r''' exit 1''')
|
||||||
|
..writeln(r''' fi''')
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(jsonEncode({'id': webhookId}))
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(
|
||||||
|
'if [[ "\$1" == "api" && "\$4" == '
|
||||||
|
'"repos/$repo/issues/$issueNumber/comments" ]]; then',
|
||||||
|
)
|
||||||
|
..writeln(r''' for arg in "$@"; do''')
|
||||||
|
..writeln(r''' :''');
|
||||||
|
if (postedBodyFile != null) {
|
||||||
|
buffer
|
||||||
|
..writeln(r''' if [[ "$arg" == body=* ]]; then''')
|
||||||
|
..writeln(
|
||||||
|
r''' printf '%s' "${arg#body=}" > '''
|
||||||
|
'"${bashScriptPath(postedBodyFile.path)}"',
|
||||||
|
)
|
||||||
|
..writeln(r''' fi''');
|
||||||
|
}
|
||||||
|
buffer
|
||||||
|
..writeln(r''' done''')
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(
|
||||||
|
jsonEncode({
|
||||||
|
'id': 703,
|
||||||
|
'html_url': 'https://example.test/comment/703',
|
||||||
|
'body': 'posted',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(
|
||||||
|
'if [[ "\$1" == "api" && "\$4" == "repos/$repo/hooks/$webhookId" ]]; then',
|
||||||
|
)
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(r'''echo "unexpected gh args: $*" >&2''')
|
||||||
|
..writeln('exit 1');
|
||||||
|
|
||||||
|
await ghScript.writeAsString(buffer.toString());
|
||||||
|
await makeScriptExecutable(ghScript);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'script_utils.dart';
|
||||||
|
|
||||||
|
Future<void> writeFakeGlabScript({
|
||||||
|
required File glabScript,
|
||||||
|
required Map<String, List<Map<String, Object?>>> issueListResponsesByRepo,
|
||||||
|
required Map<String, Map<int, List<Map<String, Object?>>>>
|
||||||
|
commentResponsesByRepo,
|
||||||
|
File? glabLog,
|
||||||
|
Map<String, Set<int>>? postCommentIssueNumbersByRepo,
|
||||||
|
File? postedBodyFile,
|
||||||
|
String? viewerLogin,
|
||||||
|
bool failViewerLookup = false,
|
||||||
|
}) async {
|
||||||
|
final normalizedPostCommentIssueNumbersByRepo =
|
||||||
|
postCommentIssueNumbersByRepo ??
|
||||||
|
<String, Set<int>>{
|
||||||
|
for (final entry in issueListResponsesByRepo.entries)
|
||||||
|
entry.key: entry.value
|
||||||
|
.map((issue) => issue['iid'])
|
||||||
|
.whereType<int>()
|
||||||
|
.toSet(),
|
||||||
|
};
|
||||||
|
|
||||||
|
final buffer = StringBuffer()
|
||||||
|
..writeln('#!/usr/bin/env bash')
|
||||||
|
..writeln('set -euo pipefail');
|
||||||
|
|
||||||
|
if (glabLog != null) {
|
||||||
|
buffer.writeln(
|
||||||
|
r'''printf '%s\n' "$*" >> '''
|
||||||
|
'"${bashScriptPath(glabLog.path)}"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.writeln(r'''if [[ "$1" == "api" && "$2" == "user" ]]; then''');
|
||||||
|
if (failViewerLookup) {
|
||||||
|
buffer
|
||||||
|
..writeln(r''' echo "viewer lookup failed" >&2''')
|
||||||
|
..writeln(' exit 1')
|
||||||
|
..writeln('fi');
|
||||||
|
} else {
|
||||||
|
buffer
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(jsonEncode({'username': viewerLogin ?? 'glab-user'}))
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final entry in issueListResponsesByRepo.entries) {
|
||||||
|
final repo = Uri.encodeComponent(entry.key);
|
||||||
|
buffer
|
||||||
|
..writeln(
|
||||||
|
r'''if [[ "$1" == "api" && "$4" == "projects/'''
|
||||||
|
'$repo'
|
||||||
|
r'''/issues?state=opened"* ]]; then''',
|
||||||
|
)
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(jsonEncode(entry.value))
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final repoEntry in commentResponsesByRepo.entries) {
|
||||||
|
final repo = Uri.encodeComponent(repoEntry.key);
|
||||||
|
for (final entry in repoEntry.value.entries) {
|
||||||
|
buffer
|
||||||
|
..writeln(
|
||||||
|
'if [[ "\$1" == "api" && "\$4" == '
|
||||||
|
'"projects/$repo/issues/${entry.key}/notes?per_page=100" ]]; then',
|
||||||
|
)
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(jsonEncode(entry.value))
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final repoEntry in normalizedPostCommentIssueNumbersByRepo.entries) {
|
||||||
|
final repo = Uri.encodeComponent(repoEntry.key);
|
||||||
|
for (final issueNumber in repoEntry.value) {
|
||||||
|
buffer
|
||||||
|
..writeln(
|
||||||
|
'if [[ "\$1" == "api" && "\$4" == '
|
||||||
|
'"projects/$repo/issues/$issueNumber/notes" ]]; then',
|
||||||
|
)
|
||||||
|
..writeln(r''' for arg in "$@"; do''')
|
||||||
|
..writeln(r''' :''');
|
||||||
|
if (postedBodyFile != null) {
|
||||||
|
buffer
|
||||||
|
..writeln(r''' if [[ "$arg" == body=* ]]; then''')
|
||||||
|
..writeln(
|
||||||
|
r''' printf '%s' "${arg#body=}" > '''
|
||||||
|
'"${bashScriptPath(postedBodyFile.path)}"',
|
||||||
|
)
|
||||||
|
..writeln(r''' fi''');
|
||||||
|
}
|
||||||
|
buffer
|
||||||
|
..writeln(r''' done''')
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(
|
||||||
|
jsonEncode({
|
||||||
|
'id': 901,
|
||||||
|
'body': 'posted',
|
||||||
|
'noteable_url':
|
||||||
|
'https://gitlab.example.test/group/subgroup/sample/issues/$issueNumber',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(r'''echo "unexpected glab args: $*" >&2''')
|
||||||
|
..writeln('exit 1');
|
||||||
|
|
||||||
|
await glabScript.writeAsString(buffer.toString());
|
||||||
|
await makeScriptExecutable(glabScript);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'script_utils.dart';
|
||||||
|
|
||||||
|
Future<void> writeGosmeeForwardScript({
|
||||||
|
required File gosmeeScript,
|
||||||
|
required String publicUrl,
|
||||||
|
required Map<String, Object?> payload,
|
||||||
|
File? gosmeeLog,
|
||||||
|
}) async {
|
||||||
|
final buffer = StringBuffer()
|
||||||
|
..writeln('#!/usr/bin/env bash')
|
||||||
|
..writeln('set -euo pipefail');
|
||||||
|
|
||||||
|
if (gosmeeLog != null) {
|
||||||
|
buffer.writeln(
|
||||||
|
r'''printf '%s\n' "$*" >> '''
|
||||||
|
'"${bashScriptPath(gosmeeLog.path)}"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(
|
||||||
|
r'''if [[ "$1" == "--output" && "$2" == "json" && "$3" == "client" && "$4" == "--new-url" ]]; then''',
|
||||||
|
)
|
||||||
|
..writeln(
|
||||||
|
r''' printf '%s\n' '''
|
||||||
|
"'$publicUrl'",
|
||||||
|
)
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(r'''if [[ "$1" == "client" ]]; then''')
|
||||||
|
..writeln(r''' url="$2"''')
|
||||||
|
..writeln(r''' target="$3"''')
|
||||||
|
..writeln(
|
||||||
|
r''' if [[ "$url" != '''
|
||||||
|
"'$publicUrl'"
|
||||||
|
r''' ]]; then''',
|
||||||
|
)
|
||||||
|
..writeln(r''' echo "unexpected gosmee url: $url" >&2''')
|
||||||
|
..writeln(' exit 1')
|
||||||
|
..writeln(r''' fi''')
|
||||||
|
..writeln(r''' curl -sS -X POST \''')
|
||||||
|
..writeln(r''' -H "Content-Type: application/json" \''')
|
||||||
|
..writeln(r''' -H "X-Gitea-Event: issue_comment" \''')
|
||||||
|
..writeln(r''' --data-binary @- "$target" <<'JSON' >/dev/null''')
|
||||||
|
..writeln(jsonEncode(payload))
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(r''' while true; do''')
|
||||||
|
..writeln(r''' sleep 60''')
|
||||||
|
..writeln(r''' done''')
|
||||||
|
..writeln('fi');
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(r'''echo "unexpected gosmee args: $*" >&2''')
|
||||||
|
..writeln('exit 1');
|
||||||
|
|
||||||
|
await gosmeeScript.writeAsString(buffer.toString());
|
||||||
|
await makeScriptExecutable(gosmeeScript);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'script_utils.dart';
|
||||||
|
|
||||||
|
Future<void> writeResponderScript(
|
||||||
|
File responderScript,
|
||||||
|
Map<String, Object?> response, {
|
||||||
|
String stderr = '',
|
||||||
|
int exitCode = 0,
|
||||||
|
}) async {
|
||||||
|
await responderScript.writeAsString('''#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cat >/dev/null
|
||||||
|
${stderr.isEmpty ? '' : "printf '%s\\n' ${jsonEncode(stderr)} >&2"}
|
||||||
|
cat <<'JSON'
|
||||||
|
${jsonEncode(response)}
|
||||||
|
JSON
|
||||||
|
exit $exitCode
|
||||||
|
''');
|
||||||
|
await makeScriptExecutable(responderScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> writeJsonlResponderScript(
|
||||||
|
File responderScript,
|
||||||
|
Map<String, Object?> response,
|
||||||
|
) async {
|
||||||
|
await responderScript.writeAsString('''#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cat >/dev/null
|
||||||
|
cat <<'JSON'
|
||||||
|
{"type":"thread.started","thread_id":"test-thread"}
|
||||||
|
{"type":"turn.started"}
|
||||||
|
{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":${jsonEncode(jsonEncode(response))}}}
|
||||||
|
{"type":"turn.completed"}
|
||||||
|
JSON
|
||||||
|
''');
|
||||||
|
await makeScriptExecutable(responderScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> writeDelayedLoggingResponderScript(
|
||||||
|
File responderScript, {
|
||||||
|
required File eventLog,
|
||||||
|
required Duration delay,
|
||||||
|
required Map<String, Object?> response,
|
||||||
|
}) async {
|
||||||
|
await responderScript.writeAsString('''#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
timestamp_ms() {
|
||||||
|
python3 - <<'PY'
|
||||||
|
import time
|
||||||
|
print(int(time.time() * 1000))
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
printf 'start:%s:%s\n' "\$CWS_ISSUE_NUMBER" "\$(timestamp_ms)" >> "${bashScriptPath(eventLog.path)}"
|
||||||
|
cat >/dev/null
|
||||||
|
sleep ${delay.inSeconds}
|
||||||
|
printf 'end:%s:%s\n' "\$CWS_ISSUE_NUMBER" "\$(timestamp_ms)" >> "${bashScriptPath(eventLog.path)}"
|
||||||
|
cat <<'JSON'
|
||||||
|
${jsonEncode(response)}
|
||||||
|
JSON
|
||||||
|
''');
|
||||||
|
await makeScriptExecutable(responderScript);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'script_utils.dart';
|
||||||
|
|
||||||
|
Future<void> writeFakeTeaScript({
|
||||||
|
required File teaScript,
|
||||||
|
required Map<String, List<Map<String, Object?>>> issueListResponsesByRepo,
|
||||||
|
required Map<String, Map<int, List<Map<String, Object?>>>>
|
||||||
|
commentResponsesByRepo,
|
||||||
|
File? teaLog,
|
||||||
|
Map<String, Set<int>>? postCommentIssueNumbersByRepo,
|
||||||
|
File? postedBodyFile,
|
||||||
|
String? viewerLogin,
|
||||||
|
bool failViewerLookup = false,
|
||||||
|
}) async {
|
||||||
|
final normalizedPostCommentIssueNumbersByRepo =
|
||||||
|
postCommentIssueNumbersByRepo ??
|
||||||
|
<String, Set<int>>{
|
||||||
|
for (final entry in issueListResponsesByRepo.entries)
|
||||||
|
entry.key: entry.value
|
||||||
|
.map((issue) => issue['index'] ?? issue['number'])
|
||||||
|
.whereType<int>()
|
||||||
|
.toSet(),
|
||||||
|
};
|
||||||
|
|
||||||
|
final buffer = StringBuffer()
|
||||||
|
..writeln('#!/usr/bin/env bash')
|
||||||
|
..writeln('set -euo pipefail');
|
||||||
|
|
||||||
|
if (teaLog != null) {
|
||||||
|
buffer.writeln(
|
||||||
|
r'''printf '%s\n' "$*" >> '''
|
||||||
|
'"${bashScriptPath(teaLog.path)}"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.writeln(r'''if [[ "$1" == "api" && "$2" == "user" ]]; then''');
|
||||||
|
if (failViewerLookup) {
|
||||||
|
buffer
|
||||||
|
..writeln(r''' echo "viewer lookup failed" >&2''')
|
||||||
|
..writeln(' exit 1')
|
||||||
|
..writeln('fi');
|
||||||
|
} else {
|
||||||
|
buffer
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(jsonEncode({'login': viewerLogin ?? 'tea-user'}))
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final entry in issueListResponsesByRepo.entries) {
|
||||||
|
final repo = entry.key;
|
||||||
|
buffer
|
||||||
|
..writeln(
|
||||||
|
r'''if [[ "$1" == "api" && "$4" == "repos/'''
|
||||||
|
'$repo'
|
||||||
|
r'''/issues?state=open"* ]]; then''',
|
||||||
|
)
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(jsonEncode(entry.value))
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final repoEntry in commentResponsesByRepo.entries) {
|
||||||
|
final repo = repoEntry.key;
|
||||||
|
for (final entry in repoEntry.value.entries) {
|
||||||
|
buffer
|
||||||
|
..writeln(
|
||||||
|
'if [[ "\$1" == "api" && "\$4" == '
|
||||||
|
'"repos/$repo/issues/${entry.key}/comments?limit=100" ]]; then',
|
||||||
|
)
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(jsonEncode(entry.value))
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final repoEntry in normalizedPostCommentIssueNumbersByRepo.entries) {
|
||||||
|
final repo = repoEntry.key;
|
||||||
|
for (final issueNumber in repoEntry.value) {
|
||||||
|
buffer
|
||||||
|
..writeln(
|
||||||
|
'if [[ "\$1" == "api" && "\$4" == '
|
||||||
|
'"repos/$repo/issues/$issueNumber/comments" ]]; then',
|
||||||
|
)
|
||||||
|
..writeln(r''' for arg in "$@"; do''')
|
||||||
|
..writeln(r''' :''');
|
||||||
|
if (postedBodyFile != null) {
|
||||||
|
buffer
|
||||||
|
..writeln(r''' if [[ "$arg" == body=* ]]; then''')
|
||||||
|
..writeln(
|
||||||
|
r''' printf '%s' "${arg#body=}" > '''
|
||||||
|
'"${bashScriptPath(postedBodyFile.path)}"',
|
||||||
|
)
|
||||||
|
..writeln(r''' fi''');
|
||||||
|
}
|
||||||
|
buffer
|
||||||
|
..writeln(r''' done''')
|
||||||
|
..writeln(" cat <<'JSON'")
|
||||||
|
..writeln(
|
||||||
|
jsonEncode({
|
||||||
|
'id': 701,
|
||||||
|
'url': 'https://gitea.example.test/comment/701',
|
||||||
|
'body': 'posted',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
..writeln('JSON')
|
||||||
|
..writeln(' exit 0')
|
||||||
|
..writeln('fi');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..writeln(r'''echo "unexpected tea args: $*" >&2''')
|
||||||
|
..writeln('exit 1');
|
||||||
|
|
||||||
|
await teaScript.writeAsString(buffer.toString());
|
||||||
|
await makeScriptExecutable(teaScript);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:git/git.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
Future<void> initGitRepo(Directory directory) async {
|
||||||
|
await GitDir.init(directory.path, allowContent: true);
|
||||||
|
await runGit([
|
||||||
|
'config',
|
||||||
|
'user.email',
|
||||||
|
'test@example.com',
|
||||||
|
], workingDirectory: directory.path);
|
||||||
|
await runGit([
|
||||||
|
'config',
|
||||||
|
'user.name',
|
||||||
|
'Test User',
|
||||||
|
], workingDirectory: directory.path);
|
||||||
|
await File(p.join(directory.path, 'README.md')).writeAsString('repo');
|
||||||
|
await runGit(['add', '.'], workingDirectory: directory.path);
|
||||||
|
await runGit(['commit', '-m', 'init'], workingDirectory: directory.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Set<String> gitContextEnvironmentKeys = {
|
||||||
|
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
|
||||||
|
'GIT_COMMON_DIR',
|
||||||
|
'GIT_DIR',
|
||||||
|
'GIT_IMPLICIT_WORK_TREE',
|
||||||
|
'GIT_INDEX_FILE',
|
||||||
|
'GIT_OBJECT_DIRECTORY',
|
||||||
|
'GIT_PREFIX',
|
||||||
|
'GIT_WORK_TREE',
|
||||||
|
};
|
||||||
|
|
||||||
|
Future<void> runGit(
|
||||||
|
List<String> arguments, {
|
||||||
|
required String workingDirectory,
|
||||||
|
}) async {
|
||||||
|
final environment = Map<String, String>.from(Platform.environment)
|
||||||
|
..removeWhere((key, _) => gitContextEnvironmentKeys.contains(key));
|
||||||
|
final result = await Process.run(
|
||||||
|
'git',
|
||||||
|
arguments,
|
||||||
|
workingDirectory: workingDirectory,
|
||||||
|
environment: environment,
|
||||||
|
);
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
throw ProcessException(
|
||||||
|
'git',
|
||||||
|
arguments,
|
||||||
|
'${result.stdout}\n${result.stderr}',
|
||||||
|
result.exitCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
Future<void> makeScriptExecutable(File script) async {
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await Process.run('chmod', ['+x', script.path]);
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
throw ProcessException(
|
||||||
|
'chmod',
|
||||||
|
['+x', script.path],
|
||||||
|
'${result.stdout}\n${result.stderr}',
|
||||||
|
result.exitCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String bashScriptPath(String hostPath) {
|
||||||
|
if (!Platform.isWindows) {
|
||||||
|
return hostPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
final normalized = hostPath.replaceAll('\\', '/');
|
||||||
|
final drivePrefixMatch = RegExp(r'^([A-Za-z]):/(.*)$').firstMatch(normalized);
|
||||||
|
if (drivePrefixMatch == null) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
final driveLetter = drivePrefixMatch.group(1)!.toLowerCase();
|
||||||
|
final rest = drivePrefixMatch.group(2)!;
|
||||||
|
return '/mnt/$driveLetter/$rest';
|
||||||
|
}
|
||||||
|
|
||||||
|
String hostPathFromScript(String scriptPath) {
|
||||||
|
if (!Platform.isWindows) {
|
||||||
|
return scriptPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
final trimmed = scriptPath.trim();
|
||||||
|
final mntPathMatch = RegExp(r'^/mnt/([a-zA-Z])/(.*)$').firstMatch(trimmed);
|
||||||
|
if (mntPathMatch == null) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
final driveLetter = mntPathMatch.group(1)!.toUpperCase();
|
||||||
|
final rest = mntPathMatch.group(2)!.replaceAll('/', '\\');
|
||||||
|
return '$driveLetter:\\$rest';
|
||||||
|
}
|
||||||
|
|
@ -1,983 +1,8 @@
|
||||||
import 'dart:convert';
|
export 'support/script_utils.dart';
|
||||||
import 'dart:io';
|
export 'support/fake_gh.dart';
|
||||||
|
export 'support/fake_tea.dart';
|
||||||
import 'package:git/git.dart';
|
export 'support/fake_glab.dart';
|
||||||
import 'package:path/path.dart' as p;
|
export 'support/fake_responders.dart';
|
||||||
|
export 'support/fake_gosmee.dart';
|
||||||
Future<void> makeScriptExecutable(File script) async {
|
export 'support/git_utils.dart';
|
||||||
if (Platform.isWindows) {
|
export 'support/cli_utils.dart';
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final result = await Process.run('chmod', ['+x', script.path]);
|
|
||||||
if (result.exitCode != 0) {
|
|
||||||
throw ProcessException(
|
|
||||||
'chmod',
|
|
||||||
['+x', script.path],
|
|
||||||
'${result.stdout}\n${result.stderr}',
|
|
||||||
result.exitCode,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String bashScriptPath(String hostPath) {
|
|
||||||
if (!Platform.isWindows) {
|
|
||||||
return hostPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
final normalized = hostPath.replaceAll('\\', '/');
|
|
||||||
final drivePrefixMatch = RegExp(r'^([A-Za-z]):/(.*)$').firstMatch(normalized);
|
|
||||||
if (drivePrefixMatch == null) {
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
final driveLetter = drivePrefixMatch.group(1)!.toLowerCase();
|
|
||||||
final rest = drivePrefixMatch.group(2)!;
|
|
||||||
return '/mnt/$driveLetter/$rest';
|
|
||||||
}
|
|
||||||
|
|
||||||
String hostPathFromScript(String scriptPath) {
|
|
||||||
if (!Platform.isWindows) {
|
|
||||||
return scriptPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
final trimmed = scriptPath.trim();
|
|
||||||
final mntPathMatch = RegExp(r'^/mnt/([a-zA-Z])/(.*)$').firstMatch(trimmed);
|
|
||||||
if (mntPathMatch == null) {
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
final driveLetter = mntPathMatch.group(1)!.toUpperCase();
|
|
||||||
final rest = mntPathMatch.group(2)!.replaceAll('/', '\\');
|
|
||||||
return '$driveLetter:\\$rest';
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> writeFakeGhScript({
|
|
||||||
required File ghScript,
|
|
||||||
required List<Map<String, Object?>> issueListResponse,
|
|
||||||
required Map<int, List<Map<String, Object?>>> commentResponses,
|
|
||||||
Map<String, List<Map<String, Object?>>>? issueListResponsesByRepo,
|
|
||||||
Map<String, Map<int, List<Map<String, Object?>>>>? commentResponsesByRepo,
|
|
||||||
File? ghLog,
|
|
||||||
int? postCommentIssueNumber,
|
|
||||||
Set<int>? postCommentIssueNumbers,
|
|
||||||
Map<String, Set<int>>? postCommentIssueNumbersByRepo,
|
|
||||||
File? postedBodyFile,
|
|
||||||
String? viewerLogin,
|
|
||||||
bool failViewerLookup = false,
|
|
||||||
}) async {
|
|
||||||
final normalizedIssueListResponsesByRepo =
|
|
||||||
issueListResponsesByRepo ??
|
|
||||||
<String, List<Map<String, Object?>>>{'owner/sample': issueListResponse};
|
|
||||||
final normalizedCommentResponsesByRepo =
|
|
||||||
commentResponsesByRepo ??
|
|
||||||
<String, Map<int, List<Map<String, Object?>>>>{
|
|
||||||
'owner/sample': commentResponses,
|
|
||||||
};
|
|
||||||
final normalizedPostCommentIssueNumbersByRepo =
|
|
||||||
postCommentIssueNumbersByRepo ??
|
|
||||||
<String, Set<int>>{
|
|
||||||
'owner/sample': {
|
|
||||||
...?postCommentIssueNumbers,
|
|
||||||
...?postCommentIssueNumber == null ? null : {postCommentIssueNumber},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
final searchItems =
|
|
||||||
normalizedIssueListResponsesByRepo.entries
|
|
||||||
.expand(
|
|
||||||
(entry) => entry.value.map(
|
|
||||||
(issue) => <String, Object?>{
|
|
||||||
...issue,
|
|
||||||
'repository_url': 'https://api.github.com/repos/${entry.key}',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(growable: false)
|
|
||||||
..sort((left, right) {
|
|
||||||
final leftUpdatedAt = left['updated_at'] as String? ?? '';
|
|
||||||
final rightUpdatedAt = right['updated_at'] as String? ?? '';
|
|
||||||
return leftUpdatedAt.compareTo(rightUpdatedAt);
|
|
||||||
});
|
|
||||||
final openSearchItems = searchItems
|
|
||||||
.where((item) => item['state'] == 'open')
|
|
||||||
.toList(growable: false);
|
|
||||||
final allSearchResponse = jsonEncode({
|
|
||||||
'total_count': searchItems.length,
|
|
||||||
'incomplete_results': false,
|
|
||||||
'items': searchItems,
|
|
||||||
});
|
|
||||||
final openSearchResponse = jsonEncode({
|
|
||||||
'total_count': openSearchItems.length,
|
|
||||||
'incomplete_results': false,
|
|
||||||
'items': openSearchItems,
|
|
||||||
});
|
|
||||||
|
|
||||||
final buffer = StringBuffer()
|
|
||||||
..writeln('#!/usr/bin/env bash')
|
|
||||||
..writeln('set -euo pipefail');
|
|
||||||
|
|
||||||
if (ghLog != null) {
|
|
||||||
buffer.writeln(
|
|
||||||
r'''printf '%s\n' "$*" >> '''
|
|
||||||
'"${bashScriptPath(ghLog.path)}"',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.writeln(r'''if [[ "$1" == "api" && "$2" == "user" ]]; then''');
|
|
||||||
if (failViewerLookup) {
|
|
||||||
buffer
|
|
||||||
..writeln(r''' echo "viewer lookup failed" >&2''')
|
|
||||||
..writeln(' exit 1')
|
|
||||||
..writeln('fi');
|
|
||||||
} else {
|
|
||||||
buffer
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(jsonEncode({'login': viewerLogin ?? 'test-user'}))
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(r'''if [[ "$1" == "api" && "$4" == "search/issues" ]]; then''')
|
|
||||||
..writeln(r''' query=""''')
|
|
||||||
..writeln(r''' for arg in "$@"; do''')
|
|
||||||
..writeln(r''' if [[ "$arg" == q=* ]]; then''')
|
|
||||||
..writeln(r''' query="${arg#q=}"''')
|
|
||||||
..writeln(r''' fi''')
|
|
||||||
..writeln(r''' done''')
|
|
||||||
..writeln(r''' if [[ "$query" == *"state:open"* ]]; then''')
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(openSearchResponse)
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(r''' else''')
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(allSearchResponse)
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(r''' fi''')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
|
|
||||||
for (final entry in normalizedIssueListResponsesByRepo.entries) {
|
|
||||||
final repo = entry.key;
|
|
||||||
buffer
|
|
||||||
..writeln(
|
|
||||||
'if [[ "\$1" == "api" && "\$4" == repos/$repo/issues\\?* ]]; then',
|
|
||||||
)
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(jsonEncode(entry.value))
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final repoEntry in normalizedCommentResponsesByRepo.entries) {
|
|
||||||
final repo = repoEntry.key;
|
|
||||||
for (final entry in repoEntry.value.entries) {
|
|
||||||
buffer
|
|
||||||
..writeln(
|
|
||||||
'if [[ "\$1" == "api" && "\$4" == '
|
|
||||||
'"repos/$repo/issues/${entry.key}/comments?per_page=100" ]]; then',
|
|
||||||
)
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(jsonEncode(entry.value))
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final repoEntry in normalizedPostCommentIssueNumbersByRepo.entries) {
|
|
||||||
final repo = repoEntry.key;
|
|
||||||
for (final issueNumber in repoEntry.value) {
|
|
||||||
buffer
|
|
||||||
..writeln(
|
|
||||||
'if [[ "\$1" == "api" && "\$4" == '
|
|
||||||
'"repos/$repo/issues/$issueNumber/comments" ]]; then',
|
|
||||||
)
|
|
||||||
..writeln(r''' for arg in "$@"; do''')
|
|
||||||
..writeln(r''' :''');
|
|
||||||
if (postedBodyFile != null) {
|
|
||||||
buffer
|
|
||||||
..writeln(r''' if [[ "$arg" == body=* ]]; then''')
|
|
||||||
..writeln(
|
|
||||||
r''' printf '%s' "${arg#body=}" > '''
|
|
||||||
'"${bashScriptPath(postedBodyFile.path)}"',
|
|
||||||
)
|
|
||||||
..writeln(r''' fi''');
|
|
||||||
}
|
|
||||||
buffer
|
|
||||||
..writeln(r''' done''')
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(
|
|
||||||
jsonEncode({
|
|
||||||
'id': 700,
|
|
||||||
'html_url': 'https://example.test/comment/700',
|
|
||||||
'body': 'posted',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(r'''echo "unexpected gh args: $*" >&2''')
|
|
||||||
..writeln('exit 1');
|
|
||||||
|
|
||||||
await ghScript.writeAsString(buffer.toString());
|
|
||||||
await makeScriptExecutable(ghScript);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> writeFlakySearchGhScript({
|
|
||||||
required File ghScript,
|
|
||||||
required File failureStateFile,
|
|
||||||
required List<Map<String, Object?>> issueListResponse,
|
|
||||||
required Map<int, List<Map<String, Object?>>> commentResponses,
|
|
||||||
File? ghLog,
|
|
||||||
}) async {
|
|
||||||
final searchItems = issueListResponse
|
|
||||||
.map(
|
|
||||||
(issue) => <String, Object?>{
|
|
||||||
...issue,
|
|
||||||
'repository_url': 'https://api.github.com/repos/owner/sample',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.toList(growable: false);
|
|
||||||
final searchResponse = jsonEncode({
|
|
||||||
'total_count': searchItems.length,
|
|
||||||
'incomplete_results': false,
|
|
||||||
'items': searchItems,
|
|
||||||
});
|
|
||||||
|
|
||||||
final buffer = StringBuffer()
|
|
||||||
..writeln('#!/usr/bin/env bash')
|
|
||||||
..writeln('set -euo pipefail');
|
|
||||||
|
|
||||||
if (ghLog != null) {
|
|
||||||
buffer.writeln(
|
|
||||||
r'''printf '%s\n' "$*" >> '''
|
|
||||||
'"${bashScriptPath(ghLog.path)}"',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(r'''if [[ "$1" == "api" && "$2" == "user" ]]; then''')
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(jsonEncode({'login': 'test-user'}))
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(r'''if [[ "$1" == "api" && "$4" == "search/issues" ]]; then''')
|
|
||||||
..writeln(
|
|
||||||
' if [[ ! -f "${bashScriptPath(failureStateFile.path)}" ]]; then',
|
|
||||||
)
|
|
||||||
..writeln(' touch "${bashScriptPath(failureStateFile.path)}"')
|
|
||||||
..writeln(r''' echo "error connecting to api.github.com" >&2''')
|
|
||||||
..writeln(
|
|
||||||
r''' echo "check your internet connection or https://githubstatus.com" >&2''',
|
|
||||||
)
|
|
||||||
..writeln(' exit 1')
|
|
||||||
..writeln(r''' fi''')
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(searchResponse)
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
|
|
||||||
for (final entry in commentResponses.entries) {
|
|
||||||
buffer
|
|
||||||
..writeln(
|
|
||||||
'if [[ "\$1" == "api" && "\$4" == '
|
|
||||||
'"repos/owner/sample/issues/${entry.key}/comments?per_page=100" ]]; then',
|
|
||||||
)
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(jsonEncode(entry.value))
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(r'''echo "unexpected gh args: $*" >&2''')
|
|
||||||
..writeln('exit 1');
|
|
||||||
|
|
||||||
await ghScript.writeAsString(buffer.toString());
|
|
||||||
await makeScriptExecutable(ghScript);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> writeFakeTeaScript({
|
|
||||||
required File teaScript,
|
|
||||||
required Map<String, List<Map<String, Object?>>> issueListResponsesByRepo,
|
|
||||||
required Map<String, Map<int, List<Map<String, Object?>>>>
|
|
||||||
commentResponsesByRepo,
|
|
||||||
File? teaLog,
|
|
||||||
Map<String, Set<int>>? postCommentIssueNumbersByRepo,
|
|
||||||
File? postedBodyFile,
|
|
||||||
String? viewerLogin,
|
|
||||||
bool failViewerLookup = false,
|
|
||||||
}) async {
|
|
||||||
final normalizedPostCommentIssueNumbersByRepo =
|
|
||||||
postCommentIssueNumbersByRepo ??
|
|
||||||
<String, Set<int>>{
|
|
||||||
for (final entry in issueListResponsesByRepo.entries)
|
|
||||||
entry.key: entry.value
|
|
||||||
.map((issue) => issue['index'] ?? issue['number'])
|
|
||||||
.whereType<int>()
|
|
||||||
.toSet(),
|
|
||||||
};
|
|
||||||
|
|
||||||
final buffer = StringBuffer()
|
|
||||||
..writeln('#!/usr/bin/env bash')
|
|
||||||
..writeln('set -euo pipefail');
|
|
||||||
|
|
||||||
if (teaLog != null) {
|
|
||||||
buffer.writeln(
|
|
||||||
r'''printf '%s\n' "$*" >> '''
|
|
||||||
'"${bashScriptPath(teaLog.path)}"',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.writeln(r'''if [[ "$1" == "api" && "$2" == "user" ]]; then''');
|
|
||||||
if (failViewerLookup) {
|
|
||||||
buffer
|
|
||||||
..writeln(r''' echo "viewer lookup failed" >&2''')
|
|
||||||
..writeln(' exit 1')
|
|
||||||
..writeln('fi');
|
|
||||||
} else {
|
|
||||||
buffer
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(jsonEncode({'login': viewerLogin ?? 'tea-user'}))
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final entry in issueListResponsesByRepo.entries) {
|
|
||||||
final repo = entry.key;
|
|
||||||
buffer
|
|
||||||
..writeln(
|
|
||||||
r'''if [[ "$1" == "api" && "$4" == "repos/'''
|
|
||||||
'$repo'
|
|
||||||
r'''/issues?state=open"* ]]; then''',
|
|
||||||
)
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(jsonEncode(entry.value))
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final repoEntry in commentResponsesByRepo.entries) {
|
|
||||||
final repo = repoEntry.key;
|
|
||||||
for (final entry in repoEntry.value.entries) {
|
|
||||||
buffer
|
|
||||||
..writeln(
|
|
||||||
'if [[ "\$1" == "api" && "\$4" == '
|
|
||||||
'"repos/$repo/issues/${entry.key}/comments?limit=100" ]]; then',
|
|
||||||
)
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(jsonEncode(entry.value))
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final repoEntry in normalizedPostCommentIssueNumbersByRepo.entries) {
|
|
||||||
final repo = repoEntry.key;
|
|
||||||
for (final issueNumber in repoEntry.value) {
|
|
||||||
buffer
|
|
||||||
..writeln(
|
|
||||||
'if [[ "\$1" == "api" && "\$4" == '
|
|
||||||
'"repos/$repo/issues/$issueNumber/comments" ]]; then',
|
|
||||||
)
|
|
||||||
..writeln(r''' for arg in "$@"; do''')
|
|
||||||
..writeln(r''' :''');
|
|
||||||
if (postedBodyFile != null) {
|
|
||||||
buffer
|
|
||||||
..writeln(r''' if [[ "$arg" == body=* ]]; then''')
|
|
||||||
..writeln(
|
|
||||||
r''' printf '%s' "${arg#body=}" > '''
|
|
||||||
'"${bashScriptPath(postedBodyFile.path)}"',
|
|
||||||
)
|
|
||||||
..writeln(r''' fi''');
|
|
||||||
}
|
|
||||||
buffer
|
|
||||||
..writeln(r''' done''')
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(
|
|
||||||
jsonEncode({
|
|
||||||
'id': 701,
|
|
||||||
'url': 'https://gitea.example.test/comment/701',
|
|
||||||
'body': 'posted',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(r'''echo "unexpected tea args: $*" >&2''')
|
|
||||||
..writeln('exit 1');
|
|
||||||
|
|
||||||
await teaScript.writeAsString(buffer.toString());
|
|
||||||
await makeScriptExecutable(teaScript);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> writeFakeGlabScript({
|
|
||||||
required File glabScript,
|
|
||||||
required Map<String, List<Map<String, Object?>>> issueListResponsesByRepo,
|
|
||||||
required Map<String, Map<int, List<Map<String, Object?>>>>
|
|
||||||
commentResponsesByRepo,
|
|
||||||
File? glabLog,
|
|
||||||
Map<String, Set<int>>? postCommentIssueNumbersByRepo,
|
|
||||||
File? postedBodyFile,
|
|
||||||
String? viewerLogin,
|
|
||||||
bool failViewerLookup = false,
|
|
||||||
}) async {
|
|
||||||
final normalizedPostCommentIssueNumbersByRepo =
|
|
||||||
postCommentIssueNumbersByRepo ??
|
|
||||||
<String, Set<int>>{
|
|
||||||
for (final entry in issueListResponsesByRepo.entries)
|
|
||||||
entry.key: entry.value
|
|
||||||
.map((issue) => issue['iid'])
|
|
||||||
.whereType<int>()
|
|
||||||
.toSet(),
|
|
||||||
};
|
|
||||||
|
|
||||||
final buffer = StringBuffer()
|
|
||||||
..writeln('#!/usr/bin/env bash')
|
|
||||||
..writeln('set -euo pipefail');
|
|
||||||
|
|
||||||
if (glabLog != null) {
|
|
||||||
buffer.writeln(
|
|
||||||
r'''printf '%s\n' "$*" >> '''
|
|
||||||
'"${bashScriptPath(glabLog.path)}"',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.writeln(r'''if [[ "$1" == "api" && "$2" == "user" ]]; then''');
|
|
||||||
if (failViewerLookup) {
|
|
||||||
buffer
|
|
||||||
..writeln(r''' echo "viewer lookup failed" >&2''')
|
|
||||||
..writeln(' exit 1')
|
|
||||||
..writeln('fi');
|
|
||||||
} else {
|
|
||||||
buffer
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(jsonEncode({'username': viewerLogin ?? 'glab-user'}))
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final entry in issueListResponsesByRepo.entries) {
|
|
||||||
final repo = Uri.encodeComponent(entry.key);
|
|
||||||
buffer
|
|
||||||
..writeln(
|
|
||||||
r'''if [[ "$1" == "api" && "$4" == "projects/'''
|
|
||||||
'$repo'
|
|
||||||
r'''/issues?state=opened"* ]]; then''',
|
|
||||||
)
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(jsonEncode(entry.value))
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final repoEntry in commentResponsesByRepo.entries) {
|
|
||||||
final repo = Uri.encodeComponent(repoEntry.key);
|
|
||||||
for (final entry in repoEntry.value.entries) {
|
|
||||||
buffer
|
|
||||||
..writeln(
|
|
||||||
'if [[ "\$1" == "api" && "\$4" == '
|
|
||||||
'"projects/$repo/issues/${entry.key}/notes?per_page=100" ]]; then',
|
|
||||||
)
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(jsonEncode(entry.value))
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final repoEntry in normalizedPostCommentIssueNumbersByRepo.entries) {
|
|
||||||
final repo = Uri.encodeComponent(repoEntry.key);
|
|
||||||
for (final issueNumber in repoEntry.value) {
|
|
||||||
buffer
|
|
||||||
..writeln(
|
|
||||||
'if [[ "\$1" == "api" && "\$4" == '
|
|
||||||
'"projects/$repo/issues/$issueNumber/notes" ]]; then',
|
|
||||||
)
|
|
||||||
..writeln(r''' for arg in "$@"; do''')
|
|
||||||
..writeln(r''' :''');
|
|
||||||
if (postedBodyFile != null) {
|
|
||||||
buffer
|
|
||||||
..writeln(r''' if [[ "$arg" == body=* ]]; then''')
|
|
||||||
..writeln(
|
|
||||||
r''' printf '%s' "${arg#body=}" > '''
|
|
||||||
'"${bashScriptPath(postedBodyFile.path)}"',
|
|
||||||
)
|
|
||||||
..writeln(r''' fi''');
|
|
||||||
}
|
|
||||||
buffer
|
|
||||||
..writeln(r''' done''')
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(
|
|
||||||
jsonEncode({
|
|
||||||
'id': 901,
|
|
||||||
'body': 'posted',
|
|
||||||
'noteable_url':
|
|
||||||
'https://gitlab.example.test/group/subgroup/sample/issues/$issueNumber',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(r'''echo "unexpected glab args: $*" >&2''')
|
|
||||||
..writeln('exit 1');
|
|
||||||
|
|
||||||
await glabScript.writeAsString(buffer.toString());
|
|
||||||
await makeScriptExecutable(glabScript);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> writeResponderScript(
|
|
||||||
File responderScript,
|
|
||||||
Map<String, Object?> response, {
|
|
||||||
String stderr = '',
|
|
||||||
int exitCode = 0,
|
|
||||||
}) async {
|
|
||||||
await responderScript.writeAsString('''#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cat >/dev/null
|
|
||||||
${stderr.isEmpty ? '' : "printf '%s\\n' ${jsonEncode(stderr)} >&2"}
|
|
||||||
cat <<'JSON'
|
|
||||||
${jsonEncode(response)}
|
|
||||||
JSON
|
|
||||||
exit $exitCode
|
|
||||||
''');
|
|
||||||
await makeScriptExecutable(responderScript);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> writeJsonlResponderScript(
|
|
||||||
File responderScript,
|
|
||||||
Map<String, Object?> response,
|
|
||||||
) async {
|
|
||||||
await responderScript.writeAsString('''#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cat >/dev/null
|
|
||||||
cat <<'JSON'
|
|
||||||
{"type":"thread.started","thread_id":"test-thread"}
|
|
||||||
{"type":"turn.started"}
|
|
||||||
{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":${jsonEncode(jsonEncode(response))}}}
|
|
||||||
{"type":"turn.completed"}
|
|
||||||
JSON
|
|
||||||
''');
|
|
||||||
await makeScriptExecutable(responderScript);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> writeDelayedLoggingResponderScript(
|
|
||||||
File responderScript, {
|
|
||||||
required File eventLog,
|
|
||||||
required Duration delay,
|
|
||||||
required Map<String, Object?> response,
|
|
||||||
}) async {
|
|
||||||
await responderScript.writeAsString('''#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
timestamp_ms() {
|
|
||||||
python3 - <<'PY'
|
|
||||||
import time
|
|
||||||
print(int(time.time() * 1000))
|
|
||||||
PY
|
|
||||||
}
|
|
||||||
printf 'start:%s:%s\n' "\$CWS_ISSUE_NUMBER" "\$(timestamp_ms)" >> "${bashScriptPath(eventLog.path)}"
|
|
||||||
cat >/dev/null
|
|
||||||
sleep ${delay.inSeconds}
|
|
||||||
printf 'end:%s:%s\n' "\$CWS_ISSUE_NUMBER" "\$(timestamp_ms)" >> "${bashScriptPath(eventLog.path)}"
|
|
||||||
cat <<'JSON'
|
|
||||||
${jsonEncode(response)}
|
|
||||||
JSON
|
|
||||||
''');
|
|
||||||
await makeScriptExecutable(responderScript);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> writeWebhookForwardGhScript({
|
|
||||||
required File ghScript,
|
|
||||||
required String repo,
|
|
||||||
required Map<String, Object?> issue,
|
|
||||||
required List<Map<String, Object?>> comments,
|
|
||||||
List<Map<String, Object?>> issueListResponse = const [],
|
|
||||||
File? ghLog,
|
|
||||||
File? postedBodyFile,
|
|
||||||
String? viewerLogin,
|
|
||||||
}) async {
|
|
||||||
final issueNumber = issue['number'] as int;
|
|
||||||
final payload = {
|
|
||||||
'action': 'created',
|
|
||||||
'issue': issue,
|
|
||||||
'comment': comments.last,
|
|
||||||
'repository': {'full_name': repo},
|
|
||||||
};
|
|
||||||
final searchItems = issueListResponse
|
|
||||||
.map(
|
|
||||||
(searchIssue) => <String, Object?>{
|
|
||||||
...searchIssue,
|
|
||||||
'repository_url': 'https://api.github.com/repos/$repo',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.toList(growable: false);
|
|
||||||
final searchResponse = jsonEncode({
|
|
||||||
'total_count': searchItems.length,
|
|
||||||
'incomplete_results': false,
|
|
||||||
'items': searchItems,
|
|
||||||
});
|
|
||||||
|
|
||||||
final buffer = StringBuffer()
|
|
||||||
..writeln('#!/usr/bin/env bash')
|
|
||||||
..writeln('set -euo pipefail');
|
|
||||||
|
|
||||||
if (ghLog != null) {
|
|
||||||
buffer.writeln(
|
|
||||||
r'''printf '%s\n' "$*" >> '''
|
|
||||||
'"${bashScriptPath(ghLog.path)}"',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.writeln(r'''if [[ "$1" == "api" && "$2" == "user" ]]; then''');
|
|
||||||
buffer
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(jsonEncode({'login': viewerLogin ?? 'test-user'}))
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(r'''if [[ "$1" == "api" && "$4" == "search/issues" ]]; then''')
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(searchResponse)
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(
|
|
||||||
'if [[ "\$1" == "api" && "\$4" == '
|
|
||||||
'"repos/$repo/issues/$issueNumber/comments?per_page=100" ]]; then',
|
|
||||||
)
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(jsonEncode(comments))
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(
|
|
||||||
'if [[ "\$1" == "api" && "\$4" == '
|
|
||||||
'"repos/$repo/issues/$issueNumber/comments" ]]; then',
|
|
||||||
)
|
|
||||||
..writeln(r''' for arg in "$@"; do''')
|
|
||||||
..writeln(r''' :''');
|
|
||||||
if (postedBodyFile != null) {
|
|
||||||
buffer
|
|
||||||
..writeln(r''' if [[ "$arg" == body=* ]]; then''')
|
|
||||||
..writeln(
|
|
||||||
r''' printf '%s' "${arg#body=}" > '''
|
|
||||||
'"${bashScriptPath(postedBodyFile.path)}"',
|
|
||||||
)
|
|
||||||
..writeln(r''' fi''');
|
|
||||||
}
|
|
||||||
buffer
|
|
||||||
..writeln(r''' done''')
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(
|
|
||||||
jsonEncode({
|
|
||||||
'id': 702,
|
|
||||||
'html_url': 'https://example.test/comment/702',
|
|
||||||
'body': 'posted',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(r'''if [[ "$1" == "webhook" && "$2" == "forward" ]]; then''')
|
|
||||||
..writeln(r''' url=""''')
|
|
||||||
..writeln(r''' for arg in "$@"; do''')
|
|
||||||
..writeln(r''' if [[ "$arg" == --url=* ]]; then''')
|
|
||||||
..writeln(r''' url="${arg#--url=}"''')
|
|
||||||
..writeln(r''' fi''')
|
|
||||||
..writeln(r''' done''')
|
|
||||||
..writeln(r''' if [[ -z "$url" ]]; then''')
|
|
||||||
..writeln(r''' echo "missing --url" >&2''')
|
|
||||||
..writeln(' exit 1')
|
|
||||||
..writeln(r''' fi''')
|
|
||||||
..writeln(r''' curl -sS -X POST \''')
|
|
||||||
..writeln(r''' -H "Content-Type: application/json" \''')
|
|
||||||
..writeln(r''' -H "X-GitHub-Event: issue_comment" \''')
|
|
||||||
..writeln(r''' --data-binary @- "$url" <<'JSON' >/dev/null''')
|
|
||||||
..writeln(jsonEncode(payload))
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(r''' while true; do''')
|
|
||||||
..writeln(r''' sleep 60''')
|
|
||||||
..writeln(r''' done''')
|
|
||||||
..writeln('fi');
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(r'''echo "unexpected gh args: $*" >&2''')
|
|
||||||
..writeln('exit 1');
|
|
||||||
|
|
||||||
await ghScript.writeAsString(buffer.toString());
|
|
||||||
await makeScriptExecutable(ghScript);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> writeGitHubGosmeeGhScript({
|
|
||||||
required File ghScript,
|
|
||||||
required String repo,
|
|
||||||
required int webhookId,
|
|
||||||
required int issueNumber,
|
|
||||||
required List<Map<String, Object?>> comments,
|
|
||||||
List<Map<String, Object?>> issueListResponse = const [],
|
|
||||||
File? ghLog,
|
|
||||||
File? postedBodyFile,
|
|
||||||
String? viewerLogin,
|
|
||||||
}) async {
|
|
||||||
final searchItems = issueListResponse
|
|
||||||
.map(
|
|
||||||
(issue) => <String, Object?>{
|
|
||||||
...issue,
|
|
||||||
'repository_url': 'https://api.github.com/repos/$repo',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.toList(growable: false);
|
|
||||||
final searchResponse = jsonEncode({
|
|
||||||
'total_count': searchItems.length,
|
|
||||||
'incomplete_results': false,
|
|
||||||
'items': searchItems,
|
|
||||||
});
|
|
||||||
final buffer = StringBuffer()
|
|
||||||
..writeln('#!/usr/bin/env bash')
|
|
||||||
..writeln('set -euo pipefail');
|
|
||||||
|
|
||||||
if (ghLog != null) {
|
|
||||||
buffer.writeln(
|
|
||||||
r'''printf '%s\n' "$*" >> '''
|
|
||||||
'"${bashScriptPath(ghLog.path)}"',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.writeln(r'''if [[ "$1" == "api" && "$2" == "user" ]]; then''');
|
|
||||||
buffer
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(jsonEncode({'login': viewerLogin ?? 'test-user'}))
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(r'''if [[ "$1" == "api" && "$4" == "search/issues" ]]; then''')
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(searchResponse)
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(
|
|
||||||
'if [[ "\$1" == "api" && "\$4" == '
|
|
||||||
'"repos/$repo/issues/$issueNumber/comments?per_page=100" ]]; then',
|
|
||||||
)
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(jsonEncode(comments))
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln('if [[ "\$1" == "api" && "\$4" == "repos/$repo/hooks" ]]; then')
|
|
||||||
..writeln(r''' active_flag=""''')
|
|
||||||
..writeln(r''' for ((i = 1; i <= $#; i += 1)); do''')
|
|
||||||
..writeln(r''' if [[ "${!i}" == "active=true" ]]; then''')
|
|
||||||
..writeln(r''' prev_index=$((i - 1))''')
|
|
||||||
..writeln(r''' active_flag="${!prev_index}"''')
|
|
||||||
..writeln(r''' break''')
|
|
||||||
..writeln(r''' fi''')
|
|
||||||
..writeln(r''' done''')
|
|
||||||
..writeln(
|
|
||||||
r''' if [[ "$active_flag" != "-F" && "$active_flag" != "--field" ]]; then''',
|
|
||||||
)
|
|
||||||
..writeln(
|
|
||||||
r''' echo "expected active=true to be sent with -F/--field, got: $*" >&2''',
|
|
||||||
)
|
|
||||||
..writeln(r''' exit 1''')
|
|
||||||
..writeln(r''' fi''')
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(jsonEncode({'id': webhookId}))
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(
|
|
||||||
'if [[ "\$1" == "api" && "\$4" == '
|
|
||||||
'"repos/$repo/issues/$issueNumber/comments" ]]; then',
|
|
||||||
)
|
|
||||||
..writeln(r''' for arg in "$@"; do''')
|
|
||||||
..writeln(r''' :''');
|
|
||||||
if (postedBodyFile != null) {
|
|
||||||
buffer
|
|
||||||
..writeln(r''' if [[ "$arg" == body=* ]]; then''')
|
|
||||||
..writeln(
|
|
||||||
r''' printf '%s' "${arg#body=}" > '''
|
|
||||||
'"${bashScriptPath(postedBodyFile.path)}"',
|
|
||||||
)
|
|
||||||
..writeln(r''' fi''');
|
|
||||||
}
|
|
||||||
buffer
|
|
||||||
..writeln(r''' done''')
|
|
||||||
..writeln(" cat <<'JSON'")
|
|
||||||
..writeln(
|
|
||||||
jsonEncode({
|
|
||||||
'id': 703,
|
|
||||||
'html_url': 'https://example.test/comment/703',
|
|
||||||
'body': 'posted',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(
|
|
||||||
'if [[ "\$1" == "api" && "\$4" == "repos/$repo/hooks/$webhookId" ]]; then',
|
|
||||||
)
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(r'''echo "unexpected gh args: $*" >&2''')
|
|
||||||
..writeln('exit 1');
|
|
||||||
|
|
||||||
await ghScript.writeAsString(buffer.toString());
|
|
||||||
await makeScriptExecutable(ghScript);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> writeGosmeeForwardScript({
|
|
||||||
required File gosmeeScript,
|
|
||||||
required String publicUrl,
|
|
||||||
required Map<String, Object?> payload,
|
|
||||||
File? gosmeeLog,
|
|
||||||
}) async {
|
|
||||||
final buffer = StringBuffer()
|
|
||||||
..writeln('#!/usr/bin/env bash')
|
|
||||||
..writeln('set -euo pipefail');
|
|
||||||
|
|
||||||
if (gosmeeLog != null) {
|
|
||||||
buffer.writeln(
|
|
||||||
r'''printf '%s\n' "$*" >> '''
|
|
||||||
'"${bashScriptPath(gosmeeLog.path)}"',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(
|
|
||||||
r'''if [[ "$1" == "--output" && "$2" == "json" && "$3" == "client" && "$4" == "--new-url" ]]; then''',
|
|
||||||
)
|
|
||||||
..writeln(
|
|
||||||
r''' printf '%s\n' '''
|
|
||||||
"'$publicUrl'",
|
|
||||||
)
|
|
||||||
..writeln(' exit 0')
|
|
||||||
..writeln('fi');
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(r'''if [[ "$1" == "client" ]]; then''')
|
|
||||||
..writeln(r''' url="$2"''')
|
|
||||||
..writeln(r''' target="$3"''')
|
|
||||||
..writeln(
|
|
||||||
r''' if [[ "$url" != '''
|
|
||||||
"'$publicUrl'"
|
|
||||||
r''' ]]; then''',
|
|
||||||
)
|
|
||||||
..writeln(r''' echo "unexpected gosmee url: $url" >&2''')
|
|
||||||
..writeln(' exit 1')
|
|
||||||
..writeln(r''' fi''')
|
|
||||||
..writeln(r''' curl -sS -X POST \''')
|
|
||||||
..writeln(r''' -H "Content-Type: application/json" \''')
|
|
||||||
..writeln(r''' -H "X-Gitea-Event: issue_comment" \''')
|
|
||||||
..writeln(r''' --data-binary @- "$target" <<'JSON' >/dev/null''')
|
|
||||||
..writeln(jsonEncode(payload))
|
|
||||||
..writeln('JSON')
|
|
||||||
..writeln(r''' while true; do''')
|
|
||||||
..writeln(r''' sleep 60''')
|
|
||||||
..writeln(r''' done''')
|
|
||||||
..writeln('fi');
|
|
||||||
|
|
||||||
buffer
|
|
||||||
..writeln(r'''echo "unexpected gosmee args: $*" >&2''')
|
|
||||||
..writeln('exit 1');
|
|
||||||
|
|
||||||
await gosmeeScript.writeAsString(buffer.toString());
|
|
||||||
await makeScriptExecutable(gosmeeScript);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> initGitRepo(Directory directory) async {
|
|
||||||
await GitDir.init(directory.path, allowContent: true);
|
|
||||||
await runGit([
|
|
||||||
'config',
|
|
||||||
'user.email',
|
|
||||||
'test@example.com',
|
|
||||||
], workingDirectory: directory.path);
|
|
||||||
await runGit([
|
|
||||||
'config',
|
|
||||||
'user.name',
|
|
||||||
'Test User',
|
|
||||||
], workingDirectory: directory.path);
|
|
||||||
await File(p.join(directory.path, 'README.md')).writeAsString('repo');
|
|
||||||
await runGit(['add', '.'], workingDirectory: directory.path);
|
|
||||||
await runGit(['commit', '-m', 'init'], workingDirectory: directory.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Set<String> gitContextEnvironmentKeys = {
|
|
||||||
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
|
|
||||||
'GIT_COMMON_DIR',
|
|
||||||
'GIT_DIR',
|
|
||||||
'GIT_IMPLICIT_WORK_TREE',
|
|
||||||
'GIT_INDEX_FILE',
|
|
||||||
'GIT_OBJECT_DIRECTORY',
|
|
||||||
'GIT_PREFIX',
|
|
||||||
'GIT_WORK_TREE',
|
|
||||||
};
|
|
||||||
|
|
||||||
Future<void> runGit(
|
|
||||||
List<String> arguments, {
|
|
||||||
required String workingDirectory,
|
|
||||||
}) async {
|
|
||||||
final environment = Map<String, String>.from(Platform.environment)
|
|
||||||
..removeWhere((key, _) => gitContextEnvironmentKeys.contains(key));
|
|
||||||
final result = await Process.run(
|
|
||||||
'git',
|
|
||||||
arguments,
|
|
||||||
workingDirectory: workingDirectory,
|
|
||||||
environment: environment,
|
|
||||||
);
|
|
||||||
if (result.exitCode != 0) {
|
|
||||||
throw ProcessException(
|
|
||||||
'git',
|
|
||||||
arguments,
|
|
||||||
'${result.stdout}\n${result.stderr}',
|
|
||||||
result.exitCode,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue