forked from bkinnightskytw/code_work_spawner
feat: improve debug logging
This commit is contained in:
parent
566c9defa0
commit
da3b15fdd7
|
|
@ -92,12 +92,17 @@ class _CodeWorkSpawnerCommandRunner extends CommandRunner<void> {
|
||||||
);
|
);
|
||||||
for (final project in config.projects.values) {
|
for (final project in config.projects.values) {
|
||||||
final repoDirectory = Directory(project.path);
|
final repoDirectory = Directory(project.path);
|
||||||
|
final eventSource = project.issueAssistant.eventSource;
|
||||||
logger.info(
|
logger.info(
|
||||||
'project=${project.key} '
|
'project=${project.key} '
|
||||||
'repo=${project.repo} '
|
'repo=${project.repo} '
|
||||||
'path=${project.path} '
|
'path=${project.path} '
|
||||||
'exists=${await repoDirectory.exists()} '
|
'exists=${await repoDirectory.exists()} '
|
||||||
'enabled=${project.issueAssistant.enabled}',
|
'enabled=${project.issueAssistant.enabled} '
|
||||||
|
'tracker_provider=${project.provider.name} '
|
||||||
|
'scm_provider=${project.scm.provider.name} '
|
||||||
|
'event_source=${_eventSourceKindValue(eventSource.type)}'
|
||||||
|
'${_cliEventSourceSettings(eventSource)}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final app = await IssueAssistantApp.open(
|
final app = await IssueAssistantApp.open(
|
||||||
|
|
@ -121,6 +126,39 @@ class _CodeWorkSpawnerCommandRunner extends CommandRunner<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _cliEventSourceSettings(IssueEventSourceConfig eventSource) {
|
||||||
|
return switch (eventSource) {
|
||||||
|
PollingIssueEventSourceConfig(:final pollInterval) =>
|
||||||
|
' poll_interval=$pollInterval',
|
||||||
|
ChannelIssueEventSourceConfig(
|
||||||
|
:final reconciliation,
|
||||||
|
:final reconcileInterval,
|
||||||
|
) =>
|
||||||
|
' reconciliation=${_reconciliationKindValue(reconciliation)} '
|
||||||
|
'reconcile_interval=$reconcileInterval',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String _eventSourceKindValue(IssueTrackerEventSourceKind value) {
|
||||||
|
return switch (value) {
|
||||||
|
IssueTrackerEventSourceKind.ghPolling => 'gh/polling',
|
||||||
|
IssueTrackerEventSourceKind.glabPolling => 'glab/polling',
|
||||||
|
IssueTrackerEventSourceKind.teaPolling => 'tea/polling',
|
||||||
|
IssueTrackerEventSourceKind.opPolling => 'op/polling',
|
||||||
|
IssueTrackerEventSourceKind.ghGosmee => 'gh/gosmee',
|
||||||
|
IssueTrackerEventSourceKind.glabGosmee => 'glab/gosmee',
|
||||||
|
IssueTrackerEventSourceKind.ghWebhookForward => 'gh/webhook-forward',
|
||||||
|
IssueTrackerEventSourceKind.teaGosmee => 'tea/gosmee',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String _reconciliationKindValue(IssueTrackerReconciliationKind value) {
|
||||||
|
return switch (value) {
|
||||||
|
IssueTrackerReconciliationKind.startupOnly => 'startup-only',
|
||||||
|
IssueTrackerReconciliationKind.continuous => 'continuous',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class _InitConfigCommand extends Command<void> {
|
class _InitConfigCommand extends Command<void> {
|
||||||
@override
|
@override
|
||||||
String get name => 'init-config';
|
String get name => 'init-config';
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ class IssueAssistantApp {
|
||||||
database: database,
|
database: database,
|
||||||
issueTrackerClient: issueTrackerClient,
|
issueTrackerClient: issueTrackerClient,
|
||||||
issueEventSource: RoutedIssueEventSource(
|
issueEventSource: RoutedIssueEventSource(
|
||||||
|
logger: _logger,
|
||||||
sources: {
|
sources: {
|
||||||
IssueTrackerEventSourceKind.ghPolling: pollingIssueEventSource,
|
IssueTrackerEventSourceKind.ghPolling: pollingIssueEventSource,
|
||||||
IssueTrackerEventSourceKind.glabPolling: pollingIssueEventSource,
|
IssueTrackerEventSourceKind.glabPolling: pollingIssueEventSource,
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,9 @@ class IssueTrackerClient {
|
||||||
required RepositorySlug repo,
|
required RepositorySlug repo,
|
||||||
required String url,
|
required String url,
|
||||||
}) async {
|
}) async {
|
||||||
|
_logger.debug(
|
||||||
|
'gitea webhook create repo=${repo.fullName} url=$url events=issues,issue_comment',
|
||||||
|
);
|
||||||
final json = await _runGiteaJson(
|
final json = await _runGiteaJson(
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: _buildApiPath(repo, ['hooks']),
|
path: _buildApiPath(repo, ['hooks']),
|
||||||
|
|
@ -277,6 +280,9 @@ class IssueTrackerClient {
|
||||||
required RepositorySlug repo,
|
required RepositorySlug repo,
|
||||||
required String url,
|
required String url,
|
||||||
}) async {
|
}) async {
|
||||||
|
_logger.debug(
|
||||||
|
'github webhook create repo=${repo.fullName} url=$url events=issues,issue_comment',
|
||||||
|
);
|
||||||
final json = await _runGhJson([
|
final json = await _runGhJson([
|
||||||
'api',
|
'api',
|
||||||
'-X',
|
'-X',
|
||||||
|
|
@ -307,6 +313,9 @@ class IssueTrackerClient {
|
||||||
required String trackerProject,
|
required String trackerProject,
|
||||||
required String url,
|
required String url,
|
||||||
}) async {
|
}) async {
|
||||||
|
_logger.debug(
|
||||||
|
'gitlab webhook create repo=$trackerProject url=$url events=issues,note',
|
||||||
|
);
|
||||||
final json = await _runGlabJson([
|
final json = await _runGlabJson([
|
||||||
'api',
|
'api',
|
||||||
'-X',
|
'-X',
|
||||||
|
|
@ -333,6 +342,9 @@ class IssueTrackerClient {
|
||||||
required RepositorySlug repo,
|
required RepositorySlug repo,
|
||||||
required int webhookId,
|
required int webhookId,
|
||||||
}) async {
|
}) async {
|
||||||
|
_logger.debug(
|
||||||
|
'github webhook delete repo=${repo.fullName} webhook_id=$webhookId',
|
||||||
|
);
|
||||||
await _runGhJson([
|
await _runGhJson([
|
||||||
'api',
|
'api',
|
||||||
'-X',
|
'-X',
|
||||||
|
|
@ -345,6 +357,9 @@ class IssueTrackerClient {
|
||||||
required RepositorySlug repo,
|
required RepositorySlug repo,
|
||||||
required int webhookId,
|
required int webhookId,
|
||||||
}) async {
|
}) async {
|
||||||
|
_logger.debug(
|
||||||
|
'gitea webhook delete repo=${repo.fullName} webhook_id=$webhookId',
|
||||||
|
);
|
||||||
await _runGiteaJson(
|
await _runGiteaJson(
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
path: _buildApiPath(repo, ['hooks', '$webhookId']),
|
path: _buildApiPath(repo, ['hooks', '$webhookId']),
|
||||||
|
|
@ -355,6 +370,9 @@ class IssueTrackerClient {
|
||||||
required String trackerProject,
|
required String trackerProject,
|
||||||
required int webhookId,
|
required int webhookId,
|
||||||
}) async {
|
}) async {
|
||||||
|
_logger.debug(
|
||||||
|
'gitlab webhook delete repo=$trackerProject webhook_id=$webhookId',
|
||||||
|
);
|
||||||
await _runGlabJson([
|
await _runGlabJson([
|
||||||
'api',
|
'api',
|
||||||
'-X',
|
'-X',
|
||||||
|
|
@ -991,6 +1009,9 @@ class IssueTrackerClient {
|
||||||
environment['CWS_GITEA_INSECURE']?.trim() ??
|
environment['CWS_GITEA_INSECURE']?.trim() ??
|
||||||
environment['GITEA_INSECURE']?.trim();
|
environment['GITEA_INSECURE']?.trim();
|
||||||
if (host != null && host.isNotEmpty && token != null && token.isNotEmpty) {
|
if (host != null && host.isNotEmpty && token != null && token.isNotEmpty) {
|
||||||
|
_logger.debug(
|
||||||
|
'gitea config source=env host=$host insecure=${_parseBooleanFlag(insecureValue)}',
|
||||||
|
);
|
||||||
return _GiteaConfig(
|
return _GiteaConfig(
|
||||||
host: Uri.parse(host),
|
host: Uri.parse(host),
|
||||||
token: token,
|
token: token,
|
||||||
|
|
@ -1003,6 +1024,10 @@ class IssueTrackerClient {
|
||||||
environment['GITEA_LOGIN']?.trim() ??
|
environment['GITEA_LOGIN']?.trim() ??
|
||||||
environment['TEA_LOGIN']?.trim();
|
environment['TEA_LOGIN']?.trim();
|
||||||
final configPath = _teaConfigPath(environment);
|
final configPath = _teaConfigPath(environment);
|
||||||
|
_logger.debug(
|
||||||
|
'gitea config source=tea config_path=$configPath '
|
||||||
|
'requested_login=${loginName ?? "(default)"}',
|
||||||
|
);
|
||||||
final file = File(configPath);
|
final file = File(configPath);
|
||||||
if (!await file.exists()) {
|
if (!await file.exists()) {
|
||||||
throw const FileSystemException(
|
throw const FileSystemException(
|
||||||
|
|
@ -1035,6 +1060,11 @@ class IssueTrackerClient {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.debug(
|
||||||
|
'gitea config source=tea host=$loginHost '
|
||||||
|
'login=${selectedLogin["name"]?.toString().trim() ?? "(unnamed)"} '
|
||||||
|
'insecure=${_parseBooleanFlag(selectedLogin["insecure"]?.toString())}',
|
||||||
|
);
|
||||||
return _GiteaConfig(
|
return _GiteaConfig(
|
||||||
host: Uri.parse(loginHost),
|
host: Uri.parse(loginHost),
|
||||||
token: loginToken,
|
token: loginToken,
|
||||||
|
|
|
||||||
|
|
@ -38,19 +38,27 @@ abstract class IssueEventSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
class RoutedIssueEventSource implements IssueEventSource {
|
class RoutedIssueEventSource implements IssueEventSource {
|
||||||
RoutedIssueEventSource({required this.sources});
|
RoutedIssueEventSource({required this.sources, required AppLogger logger})
|
||||||
|
: _logger = logger;
|
||||||
|
|
||||||
final Map<IssueTrackerEventSourceKind, IssueEventSource> sources;
|
final Map<IssueTrackerEventSourceKind, IssueEventSource> sources;
|
||||||
|
final AppLogger _logger;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> run({
|
Future<void> run({
|
||||||
required List<ProjectConfig> projects,
|
required List<ProjectConfig> projects,
|
||||||
required IssueEventBatchHandler onBatch,
|
required IssueEventBatchHandler onBatch,
|
||||||
}) async {
|
}) async {
|
||||||
|
for (final project in projects) {
|
||||||
|
_log(_routeLogMessage(project));
|
||||||
|
}
|
||||||
final startupReconcileProjects = projects
|
final startupReconcileProjects = projects
|
||||||
.where(_shouldRunStartupReconciliation)
|
.where(_shouldRunStartupReconciliation)
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
if (startupReconcileProjects.isNotEmpty) {
|
if (startupReconcileProjects.isNotEmpty) {
|
||||||
|
_log(
|
||||||
|
'route startup_reconciliation projects=${startupReconcileProjects.map((project) => project.key).join(",")}',
|
||||||
|
);
|
||||||
await sources[IssueTrackerEventSourceKind.ghPolling]!.runOnce(
|
await sources[IssueTrackerEventSourceKind.ghPolling]!.runOnce(
|
||||||
projects: startupReconcileProjects,
|
projects: startupReconcileProjects,
|
||||||
onBatch: onBatch,
|
onBatch: onBatch,
|
||||||
|
|
@ -72,15 +80,21 @@ class RoutedIssueEventSource implements IssueEventSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final routedEntries = projectsBySource.entries
|
||||||
|
.where((entry) => entry.value.isNotEmpty)
|
||||||
|
.toList(growable: false);
|
||||||
|
for (final entry in routedEntries) {
|
||||||
|
_log(
|
||||||
|
'route source=${_eventSourceKindValue(entry.key)} '
|
||||||
|
'projects=${entry.value.map((project) => project.key).join(",")}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await Future.wait(
|
await Future.wait(
|
||||||
projectsBySource.entries
|
routedEntries.map(
|
||||||
.where((entry) => entry.value.isNotEmpty)
|
(entry) =>
|
||||||
.map(
|
sources[entry.key]!.run(projects: entry.value, onBatch: onBatch),
|
||||||
(entry) => sources[entry.key]!.run(
|
),
|
||||||
projects: entry.value,
|
|
||||||
onBatch: onBatch,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,6 +103,9 @@ class RoutedIssueEventSource implements IssueEventSource {
|
||||||
required List<ProjectConfig> projects,
|
required List<ProjectConfig> projects,
|
||||||
required IssueEventBatchHandler onBatch,
|
required IssueEventBatchHandler onBatch,
|
||||||
}) {
|
}) {
|
||||||
|
for (final project in projects) {
|
||||||
|
_log('run_once ${_routeLogMessage(project)}');
|
||||||
|
}
|
||||||
return sources[IssueTrackerEventSourceKind.ghPolling]!.runOnce(
|
return sources[IssueTrackerEventSourceKind.ghPolling]!.runOnce(
|
||||||
projects: projects,
|
projects: projects,
|
||||||
onBatch: onBatch,
|
onBatch: onBatch,
|
||||||
|
|
@ -146,6 +163,37 @@ class RoutedIssueEventSource implements IssueEventSource {
|
||||||
(project.issueAssistant.eventSource as ChannelIssueEventSourceConfig)
|
(project.issueAssistant.eventSource as ChannelIssueEventSourceConfig)
|
||||||
.reconciliation ==
|
.reconciliation ==
|
||||||
IssueTrackerReconciliationKind.continuous;
|
IssueTrackerReconciliationKind.continuous;
|
||||||
|
|
||||||
|
String _routeLogMessage(ProjectConfig project) {
|
||||||
|
final eventSource = project.issueAssistant.eventSource;
|
||||||
|
final routedSources = _registeredSourcesFor(
|
||||||
|
project,
|
||||||
|
).map(_eventSourceKindValue).join(',');
|
||||||
|
return 'route project=${project.key} '
|
||||||
|
'provider=${project.provider.name} '
|
||||||
|
'event_source=${_eventSourceKindValue(eventSource.type)} '
|
||||||
|
'startup_reconciliation=${_shouldRunStartupReconciliation(project)} '
|
||||||
|
'continuous_reconciliation=${_shouldRunContinuousReconciliation(project)} '
|
||||||
|
'${_eventSourceSettings(eventSource)} '
|
||||||
|
'routed_sources=$routedSources';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _eventSourceSettings(IssueEventSourceConfig eventSource) {
|
||||||
|
return switch (eventSource) {
|
||||||
|
PollingIssueEventSourceConfig(:final pollInterval) =>
|
||||||
|
'poll_interval=$pollInterval',
|
||||||
|
ChannelIssueEventSourceConfig(
|
||||||
|
:final reconciliation,
|
||||||
|
:final reconcileInterval,
|
||||||
|
) =>
|
||||||
|
'reconciliation=${_reconciliationKindValue(reconciliation)} '
|
||||||
|
'reconcile_interval=$reconcileInterval',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void _log(String message) {
|
||||||
|
_logger.info(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PollingIssueEventSource implements IssueEventSource {
|
class PollingIssueEventSource implements IssueEventSource {
|
||||||
|
|
@ -493,3 +541,23 @@ class _ProjectPollState {
|
||||||
DateTime nextDueAt;
|
DateTime nextDueAt;
|
||||||
bool isRunning = false;
|
bool isRunning = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _eventSourceKindValue(IssueTrackerEventSourceKind value) {
|
||||||
|
return switch (value) {
|
||||||
|
IssueTrackerEventSourceKind.ghPolling => 'gh/polling',
|
||||||
|
IssueTrackerEventSourceKind.glabPolling => 'glab/polling',
|
||||||
|
IssueTrackerEventSourceKind.teaPolling => 'tea/polling',
|
||||||
|
IssueTrackerEventSourceKind.opPolling => 'op/polling',
|
||||||
|
IssueTrackerEventSourceKind.ghGosmee => 'gh/gosmee',
|
||||||
|
IssueTrackerEventSourceKind.glabGosmee => 'glab/gosmee',
|
||||||
|
IssueTrackerEventSourceKind.ghWebhookForward => 'gh/webhook-forward',
|
||||||
|
IssueTrackerEventSourceKind.teaGosmee => 'tea/gosmee',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String _reconciliationKindValue(IssueTrackerReconciliationKind value) {
|
||||||
|
return switch (value) {
|
||||||
|
IssueTrackerReconciliationKind.startupOnly => 'startup-only',
|
||||||
|
IssueTrackerReconciliationKind.continuous => 'continuous',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,10 @@ class GiteaGosmeeIssueEventSource implements IssueEventSource {
|
||||||
_runCompleter = completer;
|
_runCompleter = completer;
|
||||||
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||||
_server = server;
|
_server = server;
|
||||||
|
_log(
|
||||||
|
'gitea/gosmee starting projects=${giteaProjects.length} '
|
||||||
|
'keys=${giteaProjects.map((project) => project.key).join(",")}',
|
||||||
|
);
|
||||||
final repoToProject = {
|
final repoToProject = {
|
||||||
for (final project in giteaProjects) project.repo: project,
|
for (final project in giteaProjects) project.repo: project,
|
||||||
};
|
};
|
||||||
|
|
@ -88,64 +92,83 @@ class GiteaGosmeeIssueEventSource implements IssueEventSource {
|
||||||
'gitea/gosmee listening local_url=$localUrl '
|
'gitea/gosmee listening local_url=$localUrl '
|
||||||
'projects=${giteaProjects.map((project) => project.key).join(",")}',
|
'projects=${giteaProjects.map((project) => project.key).join(",")}',
|
||||||
);
|
);
|
||||||
for (final project in giteaProjects) {
|
|
||||||
_log(
|
|
||||||
'gitea/gosmee project=${project.key} create_channel '
|
|
||||||
'command="$gosmeeCommand --output json client --new-url $localUrl"',
|
|
||||||
);
|
|
||||||
final publicUrl = await _createGosmeeChannelUrl(localUrl);
|
|
||||||
_log(
|
|
||||||
'gitea/gosmee project=${project.key} channel_ready '
|
|
||||||
'public_url=$publicUrl local_url=$localUrl',
|
|
||||||
);
|
|
||||||
final webhookId = await issueTrackerClient.createGiteaIssueWebhook(
|
|
||||||
repo: project.requiredRepoSlug,
|
|
||||||
url: publicUrl,
|
|
||||||
);
|
|
||||||
_webhookIdsByProject[project.key] = webhookId;
|
|
||||||
_log(
|
|
||||||
'gitea/gosmee project=${project.key} webhook_registered '
|
|
||||||
'webhook_id=$webhookId repo=${project.repo}',
|
|
||||||
);
|
|
||||||
_log(
|
|
||||||
'gitea/gosmee project=${project.key} start_client '
|
|
||||||
'command="$gosmeeCommand client $publicUrl $localUrl"',
|
|
||||||
);
|
|
||||||
final process = await commandExecutor.start(gosmeeCommand, [
|
|
||||||
'client',
|
|
||||||
publicUrl,
|
|
||||||
localUrl,
|
|
||||||
]);
|
|
||||||
_gosmeeProcesses[project.key] = process;
|
|
||||||
_pipeProcessLogs(
|
|
||||||
project: project,
|
|
||||||
stream: process.stdout,
|
|
||||||
level: 'stdout',
|
|
||||||
);
|
|
||||||
_pipeProcessLogs(
|
|
||||||
project: project,
|
|
||||||
stream: process.stderr,
|
|
||||||
level: 'stderr',
|
|
||||||
);
|
|
||||||
unawaited(
|
|
||||||
process.exitCode.then((exitCode) {
|
|
||||||
_gosmeeProcesses.remove(project.key);
|
|
||||||
_log(
|
|
||||||
'gitea/gosmee process project=${project.key} exit_code=$exitCode',
|
|
||||||
);
|
|
||||||
if (!_isClosing && !completer.isCompleted) {
|
|
||||||
completer.completeError(
|
|
||||||
StateError(
|
|
||||||
'gosmee client exited for project=${project.key} '
|
|
||||||
'with code $exitCode.',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
for (final project in giteaProjects) {
|
||||||
|
var setupStep = 'create_channel';
|
||||||
|
try {
|
||||||
|
_log(
|
||||||
|
'gitea/gosmee project=${project.key} setup_start '
|
||||||
|
'repo=${project.repo} local_url=$localUrl',
|
||||||
|
);
|
||||||
|
_log(
|
||||||
|
'gitea/gosmee project=${project.key} create_channel '
|
||||||
|
'command="$gosmeeCommand --output json client --new-url $localUrl"',
|
||||||
|
);
|
||||||
|
final publicUrl = await _createGosmeeChannelUrl(localUrl);
|
||||||
|
_log(
|
||||||
|
'gitea/gosmee project=${project.key} channel_ready '
|
||||||
|
'public_url=$publicUrl local_url=$localUrl',
|
||||||
|
);
|
||||||
|
setupStep = 'register_webhook';
|
||||||
|
final webhookId = await issueTrackerClient.createGiteaIssueWebhook(
|
||||||
|
repo: project.requiredRepoSlug,
|
||||||
|
url: publicUrl,
|
||||||
|
);
|
||||||
|
_webhookIdsByProject[project.key] = webhookId;
|
||||||
|
_log(
|
||||||
|
'gitea/gosmee project=${project.key} webhook_registered '
|
||||||
|
'webhook_id=$webhookId repo=${project.repo}',
|
||||||
|
);
|
||||||
|
setupStep = 'start_client';
|
||||||
|
_log(
|
||||||
|
'gitea/gosmee project=${project.key} start_client '
|
||||||
|
'command="$gosmeeCommand client $publicUrl $localUrl"',
|
||||||
|
);
|
||||||
|
final process = await commandExecutor.start(gosmeeCommand, [
|
||||||
|
'client',
|
||||||
|
publicUrl,
|
||||||
|
localUrl,
|
||||||
|
]);
|
||||||
|
_gosmeeProcesses[project.key] = process;
|
||||||
|
_pipeProcessLogs(
|
||||||
|
project: project,
|
||||||
|
stream: process.stdout,
|
||||||
|
level: 'stdout',
|
||||||
|
);
|
||||||
|
_pipeProcessLogs(
|
||||||
|
project: project,
|
||||||
|
stream: process.stderr,
|
||||||
|
level: 'stderr',
|
||||||
|
);
|
||||||
|
unawaited(
|
||||||
|
process.exitCode.then((exitCode) {
|
||||||
|
_gosmeeProcesses.remove(project.key);
|
||||||
|
_log(
|
||||||
|
'gitea/gosmee process project=${project.key} exit_code=$exitCode',
|
||||||
|
);
|
||||||
|
if (!_isClosing && !completer.isCompleted) {
|
||||||
|
completer.completeError(
|
||||||
|
StateError(
|
||||||
|
'gosmee client exited for project=${project.key} '
|
||||||
|
'with code $exitCode.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
_log(
|
||||||
|
'gitea/gosmee project=${project.key} setup_complete '
|
||||||
|
'repo=${project.repo}',
|
||||||
|
);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_logError(
|
||||||
|
'gitea/gosmee project=${project.key} setup_failed '
|
||||||
|
'step=$setupStep error=$error',
|
||||||
|
);
|
||||||
|
Error.throwWithStackTrace(error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await completer.future;
|
await completer.future;
|
||||||
} finally {
|
} finally {
|
||||||
await close();
|
await close();
|
||||||
|
|
@ -223,6 +246,10 @@ class GiteaGosmeeIssueEventSource implements IssueEventSource {
|
||||||
required Map<String, ProjectConfig> repoToProject,
|
required Map<String, ProjectConfig> repoToProject,
|
||||||
required IssueEventBatchHandler onBatch,
|
required IssueEventBatchHandler onBatch,
|
||||||
}) async {
|
}) async {
|
||||||
|
_logDebug(
|
||||||
|
'gitea/gosmee webhook request method=${request.method} '
|
||||||
|
'path=${request.uri.path} event=${request.headers.value("X-Gitea-Event") ?? "(missing)"}',
|
||||||
|
);
|
||||||
if (request.method != 'POST') {
|
if (request.method != 'POST') {
|
||||||
_log(
|
_log(
|
||||||
'gitea/gosmee webhook ignored method=${request.method} '
|
'gitea/gosmee webhook ignored method=${request.method} '
|
||||||
|
|
@ -295,6 +322,11 @@ class GiteaGosmeeIssueEventSource implements IssueEventSource {
|
||||||
'--new-url',
|
'--new-url',
|
||||||
localUrl,
|
localUrl,
|
||||||
]);
|
]);
|
||||||
|
_logDebug(
|
||||||
|
'gitea/gosmee channel_command_result local_url=$localUrl '
|
||||||
|
'exit_code=${result.exitCode} stdout=${_summarizeText(result.stdout.toString())} '
|
||||||
|
'stderr=${_summarizeText(result.stderr.toString())}',
|
||||||
|
);
|
||||||
if (result.exitCode != 0) {
|
if (result.exitCode != 0) {
|
||||||
throw ProcessException(
|
throw ProcessException(
|
||||||
gosmeeCommand,
|
gosmeeCommand,
|
||||||
|
|
@ -411,9 +443,21 @@ class GiteaGosmeeIssueEventSource implements IssueEventSource {
|
||||||
_logger.info(message);
|
_logger.info(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _logDebug(String message) {
|
||||||
|
_logger.debug(message);
|
||||||
|
}
|
||||||
|
|
||||||
void _logError(String message) {
|
void _logError(String message) {
|
||||||
_logger.error(message);
|
_logger.error(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _summarizeText(String value) {
|
||||||
|
final singleLine = value.trim().replaceAll(RegExp(r'\s+'), ' ');
|
||||||
|
if (singleLine.length <= 240) {
|
||||||
|
return singleLine.isEmpty ? '(empty)' : singleLine;
|
||||||
|
}
|
||||||
|
return '${singleLine.substring(0, 240)}...';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _QueuedGiteaIssue {
|
class _QueuedGiteaIssue {
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,10 @@ class GitHubGosmeeIssueEventSource implements IssueEventSource {
|
||||||
_runCompleter = completer;
|
_runCompleter = completer;
|
||||||
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||||
_server = server;
|
_server = server;
|
||||||
|
_log(
|
||||||
|
'github/gosmee starting projects=${githubProjects.length} '
|
||||||
|
'keys=${githubProjects.map((project) => project.key).join(",")}',
|
||||||
|
);
|
||||||
final repoToProject = {
|
final repoToProject = {
|
||||||
for (final project in githubProjects) project.repo: project,
|
for (final project in githubProjects) project.repo: project,
|
||||||
};
|
};
|
||||||
|
|
@ -88,64 +92,83 @@ class GitHubGosmeeIssueEventSource implements IssueEventSource {
|
||||||
'github/gosmee listening local_url=$localUrl '
|
'github/gosmee listening local_url=$localUrl '
|
||||||
'projects=${githubProjects.map((project) => project.key).join(",")}',
|
'projects=${githubProjects.map((project) => project.key).join(",")}',
|
||||||
);
|
);
|
||||||
for (final project in githubProjects) {
|
|
||||||
_log(
|
|
||||||
'github/gosmee project=${project.key} create_channel '
|
|
||||||
'command="$gosmeeCommand --output json client --new-url $localUrl"',
|
|
||||||
);
|
|
||||||
final publicUrl = await _createGosmeeChannelUrl(localUrl);
|
|
||||||
_log(
|
|
||||||
'github/gosmee project=${project.key} channel_ready '
|
|
||||||
'public_url=$publicUrl local_url=$localUrl',
|
|
||||||
);
|
|
||||||
final webhookId = await issueTrackerClient.createGitHubIssueWebhook(
|
|
||||||
repo: project.requiredRepoSlug,
|
|
||||||
url: publicUrl,
|
|
||||||
);
|
|
||||||
_webhookIdsByProject[project.key] = webhookId;
|
|
||||||
_log(
|
|
||||||
'github/gosmee project=${project.key} webhook_registered '
|
|
||||||
'webhook_id=$webhookId repo=${project.repo}',
|
|
||||||
);
|
|
||||||
_log(
|
|
||||||
'github/gosmee project=${project.key} start_client '
|
|
||||||
'command="$gosmeeCommand client $publicUrl $localUrl"',
|
|
||||||
);
|
|
||||||
final process = await commandExecutor.start(gosmeeCommand, [
|
|
||||||
'client',
|
|
||||||
publicUrl,
|
|
||||||
localUrl,
|
|
||||||
]);
|
|
||||||
_gosmeeProcesses[project.key] = process;
|
|
||||||
_pipeProcessLogs(
|
|
||||||
project: project,
|
|
||||||
stream: process.stdout,
|
|
||||||
level: 'stdout',
|
|
||||||
);
|
|
||||||
_pipeProcessLogs(
|
|
||||||
project: project,
|
|
||||||
stream: process.stderr,
|
|
||||||
level: 'stderr',
|
|
||||||
);
|
|
||||||
unawaited(
|
|
||||||
process.exitCode.then((exitCode) {
|
|
||||||
_gosmeeProcesses.remove(project.key);
|
|
||||||
_log(
|
|
||||||
'github/gosmee process project=${project.key} exit_code=$exitCode',
|
|
||||||
);
|
|
||||||
if (!_isClosing && !completer.isCompleted) {
|
|
||||||
completer.completeError(
|
|
||||||
StateError(
|
|
||||||
'gosmee client exited for project=${project.key} '
|
|
||||||
'with code $exitCode.',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
for (final project in githubProjects) {
|
||||||
|
var setupStep = 'create_channel';
|
||||||
|
try {
|
||||||
|
_log(
|
||||||
|
'github/gosmee project=${project.key} setup_start '
|
||||||
|
'repo=${project.repo} local_url=$localUrl',
|
||||||
|
);
|
||||||
|
_log(
|
||||||
|
'github/gosmee project=${project.key} create_channel '
|
||||||
|
'command="$gosmeeCommand --output json client --new-url $localUrl"',
|
||||||
|
);
|
||||||
|
final publicUrl = await _createGosmeeChannelUrl(localUrl);
|
||||||
|
_log(
|
||||||
|
'github/gosmee project=${project.key} channel_ready '
|
||||||
|
'public_url=$publicUrl local_url=$localUrl',
|
||||||
|
);
|
||||||
|
setupStep = 'register_webhook';
|
||||||
|
final webhookId = await issueTrackerClient.createGitHubIssueWebhook(
|
||||||
|
repo: project.requiredRepoSlug,
|
||||||
|
url: publicUrl,
|
||||||
|
);
|
||||||
|
_webhookIdsByProject[project.key] = webhookId;
|
||||||
|
_log(
|
||||||
|
'github/gosmee project=${project.key} webhook_registered '
|
||||||
|
'webhook_id=$webhookId repo=${project.repo}',
|
||||||
|
);
|
||||||
|
setupStep = 'start_client';
|
||||||
|
_log(
|
||||||
|
'github/gosmee project=${project.key} start_client '
|
||||||
|
'command="$gosmeeCommand client $publicUrl $localUrl"',
|
||||||
|
);
|
||||||
|
final process = await commandExecutor.start(gosmeeCommand, [
|
||||||
|
'client',
|
||||||
|
publicUrl,
|
||||||
|
localUrl,
|
||||||
|
]);
|
||||||
|
_gosmeeProcesses[project.key] = process;
|
||||||
|
_pipeProcessLogs(
|
||||||
|
project: project,
|
||||||
|
stream: process.stdout,
|
||||||
|
level: 'stdout',
|
||||||
|
);
|
||||||
|
_pipeProcessLogs(
|
||||||
|
project: project,
|
||||||
|
stream: process.stderr,
|
||||||
|
level: 'stderr',
|
||||||
|
);
|
||||||
|
unawaited(
|
||||||
|
process.exitCode.then((exitCode) {
|
||||||
|
_gosmeeProcesses.remove(project.key);
|
||||||
|
_log(
|
||||||
|
'github/gosmee process project=${project.key} exit_code=$exitCode',
|
||||||
|
);
|
||||||
|
if (!_isClosing && !completer.isCompleted) {
|
||||||
|
completer.completeError(
|
||||||
|
StateError(
|
||||||
|
'gosmee client exited for project=${project.key} '
|
||||||
|
'with code $exitCode.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
_log(
|
||||||
|
'github/gosmee project=${project.key} setup_complete '
|
||||||
|
'repo=${project.repo}',
|
||||||
|
);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_logError(
|
||||||
|
'github/gosmee project=${project.key} setup_failed '
|
||||||
|
'step=$setupStep error=$error',
|
||||||
|
);
|
||||||
|
Error.throwWithStackTrace(error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await completer.future;
|
await completer.future;
|
||||||
} finally {
|
} finally {
|
||||||
await close();
|
await close();
|
||||||
|
|
@ -223,6 +246,10 @@ class GitHubGosmeeIssueEventSource implements IssueEventSource {
|
||||||
required Map<String, ProjectConfig> repoToProject,
|
required Map<String, ProjectConfig> repoToProject,
|
||||||
required IssueEventBatchHandler onBatch,
|
required IssueEventBatchHandler onBatch,
|
||||||
}) async {
|
}) async {
|
||||||
|
_logDebug(
|
||||||
|
'github/gosmee webhook request method=${request.method} '
|
||||||
|
'path=${request.uri.path} event=${request.headers.value("X-GitHub-Event") ?? "(missing)"}',
|
||||||
|
);
|
||||||
if (request.method != 'POST') {
|
if (request.method != 'POST') {
|
||||||
_log(
|
_log(
|
||||||
'github/gosmee webhook ignored method=${request.method} '
|
'github/gosmee webhook ignored method=${request.method} '
|
||||||
|
|
@ -295,6 +322,11 @@ class GitHubGosmeeIssueEventSource implements IssueEventSource {
|
||||||
'--new-url',
|
'--new-url',
|
||||||
localUrl,
|
localUrl,
|
||||||
]);
|
]);
|
||||||
|
_logDebug(
|
||||||
|
'github/gosmee channel_command_result local_url=$localUrl '
|
||||||
|
'exit_code=${result.exitCode} stdout=${_summarizeText(result.stdout.toString())} '
|
||||||
|
'stderr=${_summarizeText(result.stderr.toString())}',
|
||||||
|
);
|
||||||
if (result.exitCode != 0) {
|
if (result.exitCode != 0) {
|
||||||
throw ProcessException(
|
throw ProcessException(
|
||||||
gosmeeCommand,
|
gosmeeCommand,
|
||||||
|
|
@ -411,7 +443,17 @@ class GitHubGosmeeIssueEventSource implements IssueEventSource {
|
||||||
|
|
||||||
void _log(String message) => _logger.info(message);
|
void _log(String message) => _logger.info(message);
|
||||||
|
|
||||||
|
void _logDebug(String message) => _logger.debug(message);
|
||||||
|
|
||||||
void _logError(String message) => _logger.error(message);
|
void _logError(String message) => _logger.error(message);
|
||||||
|
|
||||||
|
String _summarizeText(String value) {
|
||||||
|
final singleLine = value.trim().replaceAll(RegExp(r'\s+'), ' ');
|
||||||
|
if (singleLine.length <= 240) {
|
||||||
|
return singleLine.isEmpty ? '(empty)' : singleLine;
|
||||||
|
}
|
||||||
|
return '${singleLine.substring(0, 240)}...';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _QueuedGitHubIssue {
|
class _QueuedGitHubIssue {
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,10 @@ class GitHubWebhookForwardIssueEventSource implements IssueEventSource {
|
||||||
_runCompleter = completer;
|
_runCompleter = completer;
|
||||||
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||||
_server = server;
|
_server = server;
|
||||||
|
_log(
|
||||||
|
'github/webhook-forward starting projects=${githubProjects.length} '
|
||||||
|
'keys=${githubProjects.map((project) => project.key).join(",")}',
|
||||||
|
);
|
||||||
final repoToProject = {
|
final repoToProject = {
|
||||||
for (final project in githubProjects) project.repo: project,
|
for (final project in githubProjects) project.repo: project,
|
||||||
};
|
};
|
||||||
|
|
@ -79,50 +83,66 @@ class GitHubWebhookForwardIssueEventSource implements IssueEventSource {
|
||||||
'github/webhook-forward listening local_url=$url '
|
'github/webhook-forward listening local_url=$url '
|
||||||
'projects=${githubProjects.map((project) => project.key).join(",")}',
|
'projects=${githubProjects.map((project) => project.key).join(",")}',
|
||||||
);
|
);
|
||||||
for (final project in githubProjects) {
|
|
||||||
_log(
|
|
||||||
'github/webhook-forward project=${project.key} start_forwarder '
|
|
||||||
'command="${issueTrackerClient.ghCommand} webhook forward --events=issues,issue_comment --repo=${project.repo} --url=$url"',
|
|
||||||
);
|
|
||||||
final process = await commandExecutor
|
|
||||||
.start(issueTrackerClient.ghCommand, [
|
|
||||||
'webhook',
|
|
||||||
'forward',
|
|
||||||
'--events=issues,issue_comment',
|
|
||||||
'--repo=${project.repo}',
|
|
||||||
'--url=$url',
|
|
||||||
]);
|
|
||||||
_forwardProcesses[project.key] = process;
|
|
||||||
_pipeProcessLogs(
|
|
||||||
project: project,
|
|
||||||
stream: process.stdout,
|
|
||||||
level: 'stdout',
|
|
||||||
);
|
|
||||||
_pipeProcessLogs(
|
|
||||||
project: project,
|
|
||||||
stream: process.stderr,
|
|
||||||
level: 'stderr',
|
|
||||||
);
|
|
||||||
unawaited(
|
|
||||||
process.exitCode.then((exitCode) {
|
|
||||||
_forwardProcesses.remove(project.key);
|
|
||||||
_log(
|
|
||||||
'github/webhook-forward process project=${project.key} '
|
|
||||||
'exit_code=$exitCode',
|
|
||||||
);
|
|
||||||
if (!_isClosing && !completer.isCompleted) {
|
|
||||||
completer.completeError(
|
|
||||||
StateError(
|
|
||||||
'gh webhook forward exited for project=${project.key} '
|
|
||||||
'with code $exitCode.',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
for (final project in githubProjects) {
|
||||||
|
try {
|
||||||
|
_log(
|
||||||
|
'github/webhook-forward project=${project.key} setup_start '
|
||||||
|
'repo=${project.repo} local_url=$url',
|
||||||
|
);
|
||||||
|
_log(
|
||||||
|
'github/webhook-forward project=${project.key} start_forwarder '
|
||||||
|
'command="${issueTrackerClient.ghCommand} webhook forward --events=issues,issue_comment --repo=${project.repo} --url=$url"',
|
||||||
|
);
|
||||||
|
final process = await commandExecutor
|
||||||
|
.start(issueTrackerClient.ghCommand, [
|
||||||
|
'webhook',
|
||||||
|
'forward',
|
||||||
|
'--events=issues,issue_comment',
|
||||||
|
'--repo=${project.repo}',
|
||||||
|
'--url=$url',
|
||||||
|
]);
|
||||||
|
_forwardProcesses[project.key] = process;
|
||||||
|
_pipeProcessLogs(
|
||||||
|
project: project,
|
||||||
|
stream: process.stdout,
|
||||||
|
level: 'stdout',
|
||||||
|
);
|
||||||
|
_pipeProcessLogs(
|
||||||
|
project: project,
|
||||||
|
stream: process.stderr,
|
||||||
|
level: 'stderr',
|
||||||
|
);
|
||||||
|
unawaited(
|
||||||
|
process.exitCode.then((exitCode) {
|
||||||
|
_forwardProcesses.remove(project.key);
|
||||||
|
_log(
|
||||||
|
'github/webhook-forward process project=${project.key} '
|
||||||
|
'exit_code=$exitCode',
|
||||||
|
);
|
||||||
|
if (!_isClosing && !completer.isCompleted) {
|
||||||
|
completer.completeError(
|
||||||
|
StateError(
|
||||||
|
'gh webhook forward exited for project=${project.key} '
|
||||||
|
'with code $exitCode.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
_log(
|
||||||
|
'github/webhook-forward project=${project.key} setup_complete '
|
||||||
|
'repo=${project.repo}',
|
||||||
|
);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_logError(
|
||||||
|
'github/webhook-forward project=${project.key} setup_failed '
|
||||||
|
'step=start_forwarder error=$error',
|
||||||
|
);
|
||||||
|
Error.throwWithStackTrace(error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await completer.future;
|
await completer.future;
|
||||||
} finally {
|
} finally {
|
||||||
await close();
|
await close();
|
||||||
|
|
@ -179,6 +199,10 @@ class GitHubWebhookForwardIssueEventSource implements IssueEventSource {
|
||||||
required Map<String, ProjectConfig> repoToProject,
|
required Map<String, ProjectConfig> repoToProject,
|
||||||
required IssueEventBatchHandler onBatch,
|
required IssueEventBatchHandler onBatch,
|
||||||
}) async {
|
}) async {
|
||||||
|
_logDebug(
|
||||||
|
'github/webhook-forward webhook request method=${request.method} '
|
||||||
|
'path=${request.uri.path} event=${request.headers.value("X-GitHub-Event") ?? "(missing)"}',
|
||||||
|
);
|
||||||
if (request.method != 'POST') {
|
if (request.method != 'POST') {
|
||||||
_log(
|
_log(
|
||||||
'github/webhook-forward webhook ignored method=${request.method} '
|
'github/webhook-forward webhook ignored method=${request.method} '
|
||||||
|
|
@ -321,6 +345,10 @@ class GitHubWebhookForwardIssueEventSource implements IssueEventSource {
|
||||||
_logger.info(message);
|
_logger.info(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _logDebug(String message) {
|
||||||
|
_logger.debug(message);
|
||||||
|
}
|
||||||
|
|
||||||
void _logError(String message) {
|
void _logError(String message) {
|
||||||
_logger.error(message);
|
_logger.error(message);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,10 @@ class GitLabGosmeeIssueEventSource implements IssueEventSource {
|
||||||
_runCompleter = completer;
|
_runCompleter = completer;
|
||||||
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||||
_server = server;
|
_server = server;
|
||||||
|
_log(
|
||||||
|
'gitlab/gosmee starting projects=${gitLabProjects.length} '
|
||||||
|
'keys=${gitLabProjects.map((project) => project.key).join(",")}',
|
||||||
|
);
|
||||||
final repoToProject = {
|
final repoToProject = {
|
||||||
for (final project in gitLabProjects) project.repo: project,
|
for (final project in gitLabProjects) project.repo: project,
|
||||||
};
|
};
|
||||||
|
|
@ -86,64 +90,83 @@ class GitLabGosmeeIssueEventSource implements IssueEventSource {
|
||||||
'gitlab/gosmee listening local_url=$localUrl '
|
'gitlab/gosmee listening local_url=$localUrl '
|
||||||
'projects=${gitLabProjects.map((project) => project.key).join(",")}',
|
'projects=${gitLabProjects.map((project) => project.key).join(",")}',
|
||||||
);
|
);
|
||||||
for (final project in gitLabProjects) {
|
|
||||||
_log(
|
|
||||||
'gitlab/gosmee project=${project.key} create_channel '
|
|
||||||
'command="$gosmeeCommand --output json client --new-url $localUrl"',
|
|
||||||
);
|
|
||||||
final publicUrl = await _createGosmeeChannelUrl(localUrl);
|
|
||||||
_log(
|
|
||||||
'gitlab/gosmee project=${project.key} channel_ready '
|
|
||||||
'public_url=$publicUrl local_url=$localUrl',
|
|
||||||
);
|
|
||||||
final webhookId = await issueTrackerClient.createGitLabIssueWebhook(
|
|
||||||
trackerProject: project.repo,
|
|
||||||
url: publicUrl,
|
|
||||||
);
|
|
||||||
_webhookIdsByProject[project.key] = webhookId;
|
|
||||||
_log(
|
|
||||||
'gitlab/gosmee project=${project.key} webhook_registered '
|
|
||||||
'webhook_id=$webhookId repo=${project.repo}',
|
|
||||||
);
|
|
||||||
_log(
|
|
||||||
'gitlab/gosmee project=${project.key} start_client '
|
|
||||||
'command="$gosmeeCommand client $publicUrl $localUrl"',
|
|
||||||
);
|
|
||||||
final process = await commandExecutor.start(gosmeeCommand, [
|
|
||||||
'client',
|
|
||||||
publicUrl,
|
|
||||||
localUrl,
|
|
||||||
]);
|
|
||||||
_gosmeeProcesses[project.key] = process;
|
|
||||||
_pipeProcessLogs(
|
|
||||||
project: project,
|
|
||||||
stream: process.stdout,
|
|
||||||
level: 'stdout',
|
|
||||||
);
|
|
||||||
_pipeProcessLogs(
|
|
||||||
project: project,
|
|
||||||
stream: process.stderr,
|
|
||||||
level: 'stderr',
|
|
||||||
);
|
|
||||||
unawaited(
|
|
||||||
process.exitCode.then((exitCode) {
|
|
||||||
_gosmeeProcesses.remove(project.key);
|
|
||||||
_log(
|
|
||||||
'gitlab/gosmee process project=${project.key} exit_code=$exitCode',
|
|
||||||
);
|
|
||||||
if (!_isClosing && !completer.isCompleted) {
|
|
||||||
completer.completeError(
|
|
||||||
StateError(
|
|
||||||
'gosmee client exited for project=${project.key} '
|
|
||||||
'with code $exitCode.',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
for (final project in gitLabProjects) {
|
||||||
|
var setupStep = 'create_channel';
|
||||||
|
try {
|
||||||
|
_log(
|
||||||
|
'gitlab/gosmee project=${project.key} setup_start '
|
||||||
|
'repo=${project.repo} local_url=$localUrl',
|
||||||
|
);
|
||||||
|
_log(
|
||||||
|
'gitlab/gosmee project=${project.key} create_channel '
|
||||||
|
'command="$gosmeeCommand --output json client --new-url $localUrl"',
|
||||||
|
);
|
||||||
|
final publicUrl = await _createGosmeeChannelUrl(localUrl);
|
||||||
|
_log(
|
||||||
|
'gitlab/gosmee project=${project.key} channel_ready '
|
||||||
|
'public_url=$publicUrl local_url=$localUrl',
|
||||||
|
);
|
||||||
|
setupStep = 'register_webhook';
|
||||||
|
final webhookId = await issueTrackerClient.createGitLabIssueWebhook(
|
||||||
|
trackerProject: project.repo,
|
||||||
|
url: publicUrl,
|
||||||
|
);
|
||||||
|
_webhookIdsByProject[project.key] = webhookId;
|
||||||
|
_log(
|
||||||
|
'gitlab/gosmee project=${project.key} webhook_registered '
|
||||||
|
'webhook_id=$webhookId repo=${project.repo}',
|
||||||
|
);
|
||||||
|
setupStep = 'start_client';
|
||||||
|
_log(
|
||||||
|
'gitlab/gosmee project=${project.key} start_client '
|
||||||
|
'command="$gosmeeCommand client $publicUrl $localUrl"',
|
||||||
|
);
|
||||||
|
final process = await commandExecutor.start(gosmeeCommand, [
|
||||||
|
'client',
|
||||||
|
publicUrl,
|
||||||
|
localUrl,
|
||||||
|
]);
|
||||||
|
_gosmeeProcesses[project.key] = process;
|
||||||
|
_pipeProcessLogs(
|
||||||
|
project: project,
|
||||||
|
stream: process.stdout,
|
||||||
|
level: 'stdout',
|
||||||
|
);
|
||||||
|
_pipeProcessLogs(
|
||||||
|
project: project,
|
||||||
|
stream: process.stderr,
|
||||||
|
level: 'stderr',
|
||||||
|
);
|
||||||
|
unawaited(
|
||||||
|
process.exitCode.then((exitCode) {
|
||||||
|
_gosmeeProcesses.remove(project.key);
|
||||||
|
_log(
|
||||||
|
'gitlab/gosmee process project=${project.key} exit_code=$exitCode',
|
||||||
|
);
|
||||||
|
if (!_isClosing && !completer.isCompleted) {
|
||||||
|
completer.completeError(
|
||||||
|
StateError(
|
||||||
|
'gosmee client exited for project=${project.key} '
|
||||||
|
'with code $exitCode.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
_log(
|
||||||
|
'gitlab/gosmee project=${project.key} setup_complete '
|
||||||
|
'repo=${project.repo}',
|
||||||
|
);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_logError(
|
||||||
|
'gitlab/gosmee project=${project.key} setup_failed '
|
||||||
|
'step=$setupStep error=$error',
|
||||||
|
);
|
||||||
|
Error.throwWithStackTrace(error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await completer.future;
|
await completer.future;
|
||||||
} finally {
|
} finally {
|
||||||
await close();
|
await close();
|
||||||
|
|
@ -221,6 +244,10 @@ class GitLabGosmeeIssueEventSource implements IssueEventSource {
|
||||||
required Map<String, ProjectConfig> repoToProject,
|
required Map<String, ProjectConfig> repoToProject,
|
||||||
required IssueEventBatchHandler onBatch,
|
required IssueEventBatchHandler onBatch,
|
||||||
}) async {
|
}) async {
|
||||||
|
_logDebug(
|
||||||
|
'gitlab/gosmee webhook request method=${request.method} '
|
||||||
|
'path=${request.uri.path} event=${request.headers.value("X-Gitlab-Event") ?? "(missing)"}',
|
||||||
|
);
|
||||||
if (request.method != 'POST') {
|
if (request.method != 'POST') {
|
||||||
_log(
|
_log(
|
||||||
'gitlab/gosmee webhook ignored method=${request.method} '
|
'gitlab/gosmee webhook ignored method=${request.method} '
|
||||||
|
|
@ -333,6 +360,11 @@ class GitLabGosmeeIssueEventSource implements IssueEventSource {
|
||||||
'--new-url',
|
'--new-url',
|
||||||
localUrl,
|
localUrl,
|
||||||
]);
|
]);
|
||||||
|
_logDebug(
|
||||||
|
'gitlab/gosmee channel_command_result local_url=$localUrl '
|
||||||
|
'exit_code=${result.exitCode} stdout=${_summarizeText(result.stdout.toString())} '
|
||||||
|
'stderr=${_summarizeText(result.stderr.toString())}',
|
||||||
|
);
|
||||||
if (result.exitCode != 0) {
|
if (result.exitCode != 0) {
|
||||||
throw ProcessException(
|
throw ProcessException(
|
||||||
gosmeeCommand,
|
gosmeeCommand,
|
||||||
|
|
@ -449,7 +481,17 @@ class GitLabGosmeeIssueEventSource implements IssueEventSource {
|
||||||
|
|
||||||
void _log(String message) => _logger.info(message);
|
void _log(String message) => _logger.info(message);
|
||||||
|
|
||||||
|
void _logDebug(String message) => _logger.debug(message);
|
||||||
|
|
||||||
void _logError(String message) => _logger.error(message);
|
void _logError(String message) => _logger.error(message);
|
||||||
|
|
||||||
|
String _summarizeText(String value) {
|
||||||
|
final singleLine = value.trim().replaceAll(RegExp(r'\s+'), ' ');
|
||||||
|
if (singleLine.length <= 240) {
|
||||||
|
return singleLine.isEmpty ? '(empty)' : singleLine;
|
||||||
|
}
|
||||||
|
return '${singleLine.substring(0, 240)}...';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _QueuedGitLabIssue {
|
class _QueuedGitLabIssue {
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,13 @@ projects:
|
||||||
responderRunner: fakeResponderRunner(),
|
responderRunner: fakeResponderRunner(),
|
||||||
commandExecutor: fakeExternalCommandExecutor(),
|
commandExecutor: fakeExternalCommandExecutor(),
|
||||||
);
|
);
|
||||||
|
final records = <AppLogRecord>[];
|
||||||
|
void captureLog(AppLogRecord record) {
|
||||||
|
records.add(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLogger.addListener(captureLog);
|
||||||
|
addTearDown(() => AppLogger.removeListener(captureLog));
|
||||||
|
|
||||||
// When the long-running app starts and receives the forwarded webhook.
|
// When the long-running app starts and receives the forwarded webhook.
|
||||||
final runFuture = app.run();
|
final runFuture = app.run();
|
||||||
|
|
@ -168,6 +175,47 @@ projects:
|
||||||
),
|
),
|
||||||
isTrue,
|
isTrue,
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
records.any(
|
||||||
|
(record) => record.message.contains(
|
||||||
|
'route project=sample provider=github event_source=gh/gosmee',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
records.any(
|
||||||
|
(record) =>
|
||||||
|
record.message.contains('route source=gh/gosmee projects=sample'),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
records.any(
|
||||||
|
(record) => record.message.contains(
|
||||||
|
'github/gosmee project=sample setup_start',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
records.any(
|
||||||
|
(record) =>
|
||||||
|
record.level == AppLogLevel.debug &&
|
||||||
|
record.message.contains('github/gosmee channel_command_result'),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
records.any(
|
||||||
|
(record) =>
|
||||||
|
record.level == AppLogLevel.debug &&
|
||||||
|
record.message.contains(
|
||||||
|
'github webhook create repo=owner/sample',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,13 @@ projects:
|
||||||
responderRunner: fakeResponderRunner(),
|
responderRunner: fakeResponderRunner(),
|
||||||
commandExecutor: fakeExternalCommandExecutor(),
|
commandExecutor: fakeExternalCommandExecutor(),
|
||||||
);
|
);
|
||||||
|
final records = <AppLogRecord>[];
|
||||||
|
void captureLog(AppLogRecord record) {
|
||||||
|
records.add(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLogger.addListener(captureLog);
|
||||||
|
addTearDown(() => AppLogger.removeListener(captureLog));
|
||||||
|
|
||||||
// When the long-running app starts and receives the forwarded webhook.
|
// When the long-running app starts and receives the forwarded webhook.
|
||||||
final runFuture = app.run();
|
final runFuture = app.run();
|
||||||
|
|
@ -186,6 +193,48 @@ projects:
|
||||||
),
|
),
|
||||||
isTrue,
|
isTrue,
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
records.any(
|
||||||
|
(record) => record.message.contains(
|
||||||
|
'route project=sample provider=gitlab event_source=glab/gosmee',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
records.any(
|
||||||
|
(record) => record.message.contains(
|
||||||
|
'route source=glab/gosmee projects=sample',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
records.any(
|
||||||
|
(record) => record.message.contains(
|
||||||
|
'gitlab/gosmee project=sample setup_start',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
records.any(
|
||||||
|
(record) =>
|
||||||
|
record.level == AppLogLevel.debug &&
|
||||||
|
record.message.contains('gitlab/gosmee channel_command_result'),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
records.any(
|
||||||
|
(record) =>
|
||||||
|
record.level == AppLogLevel.debug &&
|
||||||
|
record.message.contains(
|
||||||
|
'gitlab webhook create repo=group/subgroup/sample',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,13 @@ CWS_GITEA_TOKEN=test-token
|
||||||
responderRunner: fakeResponderRunner(),
|
responderRunner: fakeResponderRunner(),
|
||||||
commandExecutor: fakeExternalCommandExecutor(),
|
commandExecutor: fakeExternalCommandExecutor(),
|
||||||
);
|
);
|
||||||
|
final records = <AppLogRecord>[];
|
||||||
|
void captureLog(AppLogRecord record) {
|
||||||
|
records.add(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLogger.addListener(captureLog);
|
||||||
|
addTearDown(() => AppLogger.removeListener(captureLog));
|
||||||
|
|
||||||
// When the long-running app starts and receives the forwarded webhook.
|
// When the long-running app starts and receives the forwarded webhook.
|
||||||
final runFuture = app.run();
|
final runFuture = app.run();
|
||||||
|
|
@ -163,6 +170,46 @@ CWS_GITEA_TOKEN=test-token
|
||||||
'https://gosmee.example.test/sample',
|
'https://gosmee.example.test/sample',
|
||||||
);
|
);
|
||||||
expect(giteaServer.deletedWebhookIds, contains(81));
|
expect(giteaServer.deletedWebhookIds, contains(81));
|
||||||
|
expect(
|
||||||
|
records.any(
|
||||||
|
(record) => record.message.contains(
|
||||||
|
'route project=sample provider=gitea event_source=tea/gosmee',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
records.any(
|
||||||
|
(record) => record.message.contains(
|
||||||
|
'route source=tea/gosmee projects=sample',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
records.any(
|
||||||
|
(record) => record.message.contains(
|
||||||
|
'gitea/gosmee project=sample setup_start',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
records.any(
|
||||||
|
(record) =>
|
||||||
|
record.level == AppLogLevel.debug &&
|
||||||
|
record.message.contains('gitea/gosmee channel_command_result'),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
records.any(
|
||||||
|
(record) =>
|
||||||
|
record.level == AppLogLevel.debug &&
|
||||||
|
record.message.contains('gitea config source=env'),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/// ```gherkin
|
/// ```gherkin
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ void main() {
|
||||||
final polling = _RecordingIssueEventSource();
|
final polling = _RecordingIssueEventSource();
|
||||||
final webhookForward = _RecordingIssueEventSource();
|
final webhookForward = _RecordingIssueEventSource();
|
||||||
final routed = RoutedIssueEventSource(
|
final routed = RoutedIssueEventSource(
|
||||||
|
logger: AppLogger('test.routed_issue_event_source'),
|
||||||
sources: {
|
sources: {
|
||||||
IssueTrackerEventSourceKind.ghPolling: polling,
|
IssueTrackerEventSourceKind.ghPolling: polling,
|
||||||
IssueTrackerEventSourceKind.teaPolling: polling,
|
IssueTrackerEventSourceKind.teaPolling: polling,
|
||||||
|
|
@ -63,6 +64,7 @@ void main() {
|
||||||
final polling = _RecordingIssueEventSource();
|
final polling = _RecordingIssueEventSource();
|
||||||
final webhookForward = _RecordingIssueEventSource();
|
final webhookForward = _RecordingIssueEventSource();
|
||||||
final routed = RoutedIssueEventSource(
|
final routed = RoutedIssueEventSource(
|
||||||
|
logger: AppLogger('test.routed_issue_event_source'),
|
||||||
sources: {
|
sources: {
|
||||||
IssueTrackerEventSourceKind.ghPolling: polling,
|
IssueTrackerEventSourceKind.ghPolling: polling,
|
||||||
IssueTrackerEventSourceKind.teaPolling: polling,
|
IssueTrackerEventSourceKind.teaPolling: polling,
|
||||||
|
|
|
||||||
|
|
@ -517,6 +517,7 @@ logins:
|
||||||
..write('[]');
|
..write('[]');
|
||||||
await request.response.close();
|
await request.response.close();
|
||||||
});
|
});
|
||||||
|
final serverHost = server.address.host;
|
||||||
_configureGiteaTestEnvironment(server);
|
_configureGiteaTestEnvironment(server);
|
||||||
final logMessages = <String>[];
|
final logMessages = <String>[];
|
||||||
void listener(AppLogRecord record) {
|
void listener(AppLogRecord record) {
|
||||||
|
|
@ -555,6 +556,12 @@ logins:
|
||||||
}
|
}
|
||||||
|
|
||||||
// And the tracker logger records the timed out request.
|
// And the tracker logger records the timed out request.
|
||||||
|
expect(
|
||||||
|
logMessages,
|
||||||
|
contains(
|
||||||
|
allOf(contains('gitea config source=env'), contains(serverHost)),
|
||||||
|
),
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
logMessages,
|
logMessages,
|
||||||
contains(
|
contains(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue