forked from bkinnightskytw/code_work_spawner
fix: infer tracker provider from event source (#38)
This commit is contained in:
parent
f1eea7abd8
commit
da256d068c
|
|
@ -6,11 +6,13 @@ Project entries can override `issueAssistant` fields when a repo needs a
|
||||||
different prompt, mention triggers, responder chain, notification list, or
|
different prompt, mention triggers, responder chain, notification list, or
|
||||||
event source.
|
event source.
|
||||||
|
|
||||||
Each project also has a separate SCM config. `issueTracker.provider` selects
|
Each project also has separate tracker and SCM config. `issueTracker.provider`
|
||||||
the issue tracker integration, while `scm.provider` selects how temporary repo
|
selects the logical tracker family, `issueTracker.eventSource.type` selects the
|
||||||
workspaces are prepared for responders. Legacy top-level project `provider` is
|
concrete adapter used to receive updates, and `scm.provider` selects how
|
||||||
still accepted during load for backward compatibility. When `scm` is omitted
|
temporary repo workspaces are prepared for responders. Legacy top-level project
|
||||||
it defaults to a matching provider for existing GitHub, GitLab, and Gitea configs.
|
`provider` and legacy `issueAssistant.eventSource` are still accepted during
|
||||||
|
load for backward compatibility. When `scm` is omitted it defaults to a
|
||||||
|
matching provider for existing GitHub, GitLab, and Gitea configs.
|
||||||
|
|
||||||
GitHub projects default to `gh/polling`, GitLab projects default to
|
GitHub projects default to `gh/polling`, GitLab projects default to
|
||||||
`glab/polling`, and Gitea projects default to `tea/polling`. OpenProject
|
`glab/polling`, and Gitea projects default to `tea/polling`. OpenProject
|
||||||
|
|
@ -32,11 +34,12 @@ recovery, or use `reconciliation: startup-only` to skip background polling
|
||||||
after startup. `--once` still uses a single polling reconciliation cycle so
|
after startup. `--once` still uses a single polling reconciliation cycle so
|
||||||
existing backlog handling keeps working.
|
existing backlog handling keeps working.
|
||||||
|
|
||||||
Use a nested `issueAssistant.eventSource` object so source-specific fields stay
|
Use a nested `issueTracker.eventSource` object so adapter-specific fields stay
|
||||||
grouped with the selected source type. For example:
|
grouped with the selected adapter key. For example:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
issueAssistant:
|
issueTracker:
|
||||||
|
provider: github
|
||||||
eventSource:
|
eventSource:
|
||||||
type: gh/webhook-forward
|
type: gh/webhook-forward
|
||||||
reconciliation: continuous
|
reconciliation: continuous
|
||||||
|
|
@ -44,7 +47,8 @@ issueAssistant:
|
||||||
```
|
```
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
issueAssistant:
|
issueTracker:
|
||||||
|
provider: github
|
||||||
eventSource:
|
eventSource:
|
||||||
type: gh/polling
|
type: gh/polling
|
||||||
pollInterval: 30s
|
pollInterval: 30s
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,6 @@ worktreeDir: ~/.worktrees
|
||||||
defaults:
|
defaults:
|
||||||
issueAssistant:
|
issueAssistant:
|
||||||
enabled: true
|
enabled: true
|
||||||
# Use tea/gosmee for long-running runs.
|
|
||||||
eventSource:
|
|
||||||
type: tea/polling
|
|
||||||
pollInterval: 30s
|
|
||||||
# eventSource:
|
|
||||||
# type: tea/gosmee
|
|
||||||
# reconciliation: startup-only
|
|
||||||
# reconcileInterval: 30s
|
|
||||||
maxConcurrentIssues: 3
|
maxConcurrentIssues: 3
|
||||||
mentionTriggers: ["@codex", "@helper"]
|
mentionTriggers: ["@codex", "@helper"]
|
||||||
responders:
|
responders:
|
||||||
|
|
@ -27,6 +19,14 @@ projects:
|
||||||
sample-gitea:
|
sample-gitea:
|
||||||
issueTracker:
|
issueTracker:
|
||||||
provider: gitea
|
provider: gitea
|
||||||
|
# Use tea/gosmee for long-running runs.
|
||||||
|
eventSource:
|
||||||
|
type: tea/polling
|
||||||
|
pollInterval: 30s
|
||||||
|
# eventSource:
|
||||||
|
# type: tea/gosmee
|
||||||
|
# reconciliation: startup-only
|
||||||
|
# reconcileInterval: 30s
|
||||||
scm:
|
scm:
|
||||||
provider: gitea
|
provider: gitea
|
||||||
repo: owner/repo
|
repo: owner/repo
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,6 @@ worktreeDir: ~/.worktrees
|
||||||
defaults:
|
defaults:
|
||||||
issueAssistant:
|
issueAssistant:
|
||||||
enabled: true
|
enabled: true
|
||||||
# Use gh/gosmee or gh/webhook-forward for long-running runs.
|
|
||||||
eventSource:
|
|
||||||
type: gh/polling
|
|
||||||
pollInterval: 30s
|
|
||||||
# eventSource:
|
|
||||||
# type: gh/gosmee
|
|
||||||
# reconciliation: startup-only
|
|
||||||
# reconcileInterval: 30s
|
|
||||||
maxConcurrentIssues: 3
|
maxConcurrentIssues: 3
|
||||||
mentionTriggers: ["@codex", "@helper"]
|
mentionTriggers: ["@codex", "@helper"]
|
||||||
commentPrefixTemplate: |
|
commentPrefixTemplate: |
|
||||||
|
|
@ -53,6 +45,14 @@ projects:
|
||||||
code-work-spawner:
|
code-work-spawner:
|
||||||
issueTracker:
|
issueTracker:
|
||||||
provider: github
|
provider: github
|
||||||
|
# Use gh/gosmee or gh/webhook-forward for long-running runs.
|
||||||
|
eventSource:
|
||||||
|
type: gh/polling
|
||||||
|
pollInterval: 30s
|
||||||
|
# eventSource:
|
||||||
|
# type: gh/gosmee
|
||||||
|
# reconciliation: startup-only
|
||||||
|
# reconcileInterval: 30s
|
||||||
scm:
|
scm:
|
||||||
provider: github
|
provider: github
|
||||||
repo: existedinnettw/code_work_spawner
|
repo: existedinnettw/code_work_spawner
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,6 @@ worktreeDir: ~/.worktrees
|
||||||
defaults:
|
defaults:
|
||||||
issueAssistant:
|
issueAssistant:
|
||||||
enabled: true
|
enabled: true
|
||||||
# GitLab currently supports polling.
|
|
||||||
eventSource:
|
|
||||||
type: glab/polling
|
|
||||||
pollInterval: 30s
|
|
||||||
maxConcurrentIssues: 3
|
maxConcurrentIssues: 3
|
||||||
mentionTriggers: ["@codex", "@helper"]
|
mentionTriggers: ["@codex", "@helper"]
|
||||||
responders:
|
responders:
|
||||||
|
|
@ -23,6 +19,10 @@ projects:
|
||||||
sample-gitlab:
|
sample-gitlab:
|
||||||
issueTracker:
|
issueTracker:
|
||||||
provider: gitlab
|
provider: gitlab
|
||||||
|
# GitLab currently supports polling.
|
||||||
|
eventSource:
|
||||||
|
type: glab/polling
|
||||||
|
pollInterval: 30s
|
||||||
scm:
|
scm:
|
||||||
provider: gitlab
|
provider: gitlab
|
||||||
repo: group/subgroup/repo
|
repo: group/subgroup/repo
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,6 @@ worktreeDir: ~/.worktrees
|
||||||
defaults:
|
defaults:
|
||||||
issueAssistant:
|
issueAssistant:
|
||||||
enabled: true
|
enabled: true
|
||||||
eventSource:
|
|
||||||
type: op/polling
|
|
||||||
pollInterval: 30s
|
|
||||||
maxConcurrentIssues: 3
|
maxConcurrentIssues: 3
|
||||||
mentionTriggers: ["@codex", "@helper"]
|
mentionTriggers: ["@codex", "@helper"]
|
||||||
responders:
|
responders:
|
||||||
|
|
@ -23,6 +20,9 @@ projects:
|
||||||
sample-openproject:
|
sample-openproject:
|
||||||
issueTracker:
|
issueTracker:
|
||||||
provider: openproject
|
provider: openproject
|
||||||
|
eventSource:
|
||||||
|
type: op/polling
|
||||||
|
pollInterval: 30s
|
||||||
scm:
|
scm:
|
||||||
provider: github
|
provider: github
|
||||||
# OpenProject project name or numeric project id.
|
# OpenProject project name or numeric project id.
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ import 'config_schema.dart';
|
||||||
export 'config_schema.dart'
|
export 'config_schema.dart'
|
||||||
show
|
show
|
||||||
AssistantCapability,
|
AssistantCapability,
|
||||||
IssueAssistantEventSourceKind,
|
IssueTrackerEventSourceKind,
|
||||||
IssueAssistantReconciliationKind,
|
IssueTrackerReconciliationKind,
|
||||||
IssueTrackerProvider,
|
IssueTrackerProvider,
|
||||||
ScmProvider,
|
ScmProvider,
|
||||||
NotificationConfigDocument,
|
NotificationConfigDocument,
|
||||||
|
|
@ -139,7 +139,7 @@ class AppConfig {
|
||||||
issueAssistant: IssueAssistantConfigDocument(
|
issueAssistant: IssueAssistantConfigDocument(
|
||||||
enabled: true,
|
enabled: true,
|
||||||
eventSource: IssueAssistantEventSourceDocument(
|
eventSource: IssueAssistantEventSourceDocument(
|
||||||
type: IssueAssistantEventSourceKind.ghPolling,
|
type: IssueTrackerEventSourceKind.ghPolling,
|
||||||
pollInterval: '30s',
|
pollInterval: '30s',
|
||||||
),
|
),
|
||||||
maxConcurrentIssues: 3,
|
maxConcurrentIssues: 3,
|
||||||
|
|
@ -202,10 +202,33 @@ class ProjectConfig {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final provider =
|
final resolvedIssueAssistantDocument =
|
||||||
document.issueTracker?.provider ??
|
_resolveProjectIssueAssistantDocument(
|
||||||
document.provider ??
|
key: key,
|
||||||
IssueTrackerProvider.github;
|
issueTracker: document.issueTracker,
|
||||||
|
issueAssistant: document.issueAssistant,
|
||||||
|
);
|
||||||
|
final hasExplicitEventSourceType =
|
||||||
|
resolvedIssueAssistantDocument?.eventSource?.type != null;
|
||||||
|
var issueAssistant = defaultAssistant.merge(resolvedIssueAssistantDocument);
|
||||||
|
final configuredProvider =
|
||||||
|
document.issueTracker?.provider ?? document.provider;
|
||||||
|
final inferredProvider = _issueTrackerProviderForEventSource(
|
||||||
|
issueAssistant.eventSource.type,
|
||||||
|
);
|
||||||
|
if (hasExplicitEventSourceType &&
|
||||||
|
configuredProvider != null &&
|
||||||
|
configuredProvider != inferredProvider) {
|
||||||
|
throw FormatException(
|
||||||
|
'projects.$key.issueAssistant.eventSource '
|
||||||
|
'${_eventSourceConfigValue(issueAssistant.eventSource.type)} '
|
||||||
|
'implies issue tracker provider ${inferredProvider.name}, '
|
||||||
|
'but the legacy configured provider is ${configuredProvider.name}.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final provider = hasExplicitEventSourceType
|
||||||
|
? inferredProvider
|
||||||
|
: configuredProvider ?? inferredProvider;
|
||||||
if (provider == IssueTrackerProvider.gitlab) {
|
if (provider == IssueTrackerProvider.gitlab) {
|
||||||
_validateGitLabProjectPath(repo, field: 'projects.$key.repo');
|
_validateGitLabProjectPath(repo, field: 'projects.$key.repo');
|
||||||
}
|
}
|
||||||
|
|
@ -213,15 +236,14 @@ class ProjectConfig {
|
||||||
if (document.scm?.provider == null && fallbackScmProvider == null) {
|
if (document.scm?.provider == null && fallbackScmProvider == null) {
|
||||||
throw FormatException(
|
throw FormatException(
|
||||||
'projects.$key.scm.provider is required for '
|
'projects.$key.scm.provider is required for '
|
||||||
'issueTracker.provider: ${provider.name}.',
|
'issue tracking provider: ${provider.name}.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final scm = ScmConfig.fromDocument(
|
final scm = ScmConfig.fromDocument(
|
||||||
document.scm,
|
document.scm,
|
||||||
fallbackProvider: fallbackScmProvider,
|
fallbackProvider: fallbackScmProvider,
|
||||||
);
|
);
|
||||||
var issueAssistant = defaultAssistant.merge(document.issueAssistant);
|
if (!hasExplicitEventSourceType &&
|
||||||
if (document.issueAssistant?.eventSource?.type == null &&
|
|
||||||
issueAssistant.eventSource is PollingIssueEventSourceConfig) {
|
issueAssistant.eventSource is PollingIssueEventSourceConfig) {
|
||||||
issueAssistant = issueAssistant.copyWith(
|
issueAssistant = issueAssistant.copyWith(
|
||||||
eventSource:
|
eventSource:
|
||||||
|
|
@ -229,58 +251,17 @@ class ProjectConfig {
|
||||||
.copyWith(
|
.copyWith(
|
||||||
type: switch (provider) {
|
type: switch (provider) {
|
||||||
IssueTrackerProvider.github =>
|
IssueTrackerProvider.github =>
|
||||||
IssueAssistantEventSourceKind.ghPolling,
|
IssueTrackerEventSourceKind.ghPolling,
|
||||||
IssueTrackerProvider.gitlab =>
|
IssueTrackerProvider.gitlab =>
|
||||||
IssueAssistantEventSourceKind.glabPolling,
|
IssueTrackerEventSourceKind.glabPolling,
|
||||||
IssueTrackerProvider.gitea =>
|
IssueTrackerProvider.gitea =>
|
||||||
IssueAssistantEventSourceKind.teaPolling,
|
IssueTrackerEventSourceKind.teaPolling,
|
||||||
IssueTrackerProvider.openproject =>
|
IssueTrackerProvider.openproject =>
|
||||||
IssueAssistantEventSourceKind.opPolling,
|
IssueTrackerEventSourceKind.opPolling,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
switch (issueAssistant.eventSource.type) {
|
|
||||||
case IssueAssistantEventSourceKind.ghPolling:
|
|
||||||
case IssueAssistantEventSourceKind.ghGosmee:
|
|
||||||
case IssueAssistantEventSourceKind.ghWebhookForward:
|
|
||||||
if (provider != IssueTrackerProvider.github) {
|
|
||||||
throw FormatException(
|
|
||||||
'projects.$key.issueAssistant.eventSource '
|
|
||||||
'${_eventSourceConfigValue(issueAssistant.eventSource.type)} '
|
|
||||||
'requires issueTracker.provider: github.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case IssueAssistantEventSourceKind.glabPolling:
|
|
||||||
if (provider != IssueTrackerProvider.gitlab) {
|
|
||||||
throw FormatException(
|
|
||||||
'projects.$key.issueAssistant.eventSource glab/polling '
|
|
||||||
'requires issueTracker.provider: gitlab.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case IssueAssistantEventSourceKind.teaPolling:
|
|
||||||
if (provider != IssueTrackerProvider.gitea) {
|
|
||||||
throw FormatException(
|
|
||||||
'projects.$key.issueAssistant.eventSource tea/polling '
|
|
||||||
'requires issueTracker.provider: gitea.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case IssueAssistantEventSourceKind.teaGosmee:
|
|
||||||
if (provider != IssueTrackerProvider.gitea) {
|
|
||||||
throw FormatException(
|
|
||||||
'projects.$key.issueAssistant.eventSource tea/gosmee '
|
|
||||||
'requires issueTracker.provider: gitea.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case IssueAssistantEventSourceKind.opPolling:
|
|
||||||
if (provider != IssueTrackerProvider.openproject) {
|
|
||||||
throw FormatException(
|
|
||||||
'projects.$key.issueAssistant.eventSource op/polling '
|
|
||||||
'requires issueTracker.provider: openproject.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider == IssueTrackerProvider.openproject && repo.trim().isEmpty) {
|
if (provider == IssueTrackerProvider.openproject && repo.trim().isEmpty) {
|
||||||
throw FormatException(
|
throw FormatException(
|
||||||
'Invalid OpenProject project reference for projects.$key.repo: '
|
'Invalid OpenProject project reference for projects.$key.repo: '
|
||||||
|
|
@ -343,6 +324,49 @@ class ProjectConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IssueAssistantConfigDocument? _resolveProjectIssueAssistantDocument({
|
||||||
|
required String key,
|
||||||
|
required IssueTrackerConfigDocument? issueTracker,
|
||||||
|
required IssueAssistantConfigDocument? issueAssistant,
|
||||||
|
}) {
|
||||||
|
final trackerEventSource = issueTracker?.eventSource;
|
||||||
|
final assistantEventSource = issueAssistant?.eventSource;
|
||||||
|
if (trackerEventSource != null &&
|
||||||
|
assistantEventSource != null &&
|
||||||
|
!_eventSourceDocumentsMatch(trackerEventSource, assistantEventSource)) {
|
||||||
|
throw FormatException(
|
||||||
|
'projects.$key.issueTracker.eventSource and '
|
||||||
|
'projects.$key.issueAssistant.eventSource must match when both are set.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (trackerEventSource == null) {
|
||||||
|
return issueAssistant;
|
||||||
|
}
|
||||||
|
return IssueAssistantConfigDocument(
|
||||||
|
enabled: issueAssistant?.enabled,
|
||||||
|
eventSource: trackerEventSource,
|
||||||
|
maxConcurrentIssues: issueAssistant?.maxConcurrentIssues,
|
||||||
|
mentionTriggers: issueAssistant?.mentionTriggers,
|
||||||
|
prompt: issueAssistant?.prompt,
|
||||||
|
responders: issueAssistant?.responders,
|
||||||
|
capabilities: issueAssistant?.capabilities,
|
||||||
|
notifications: issueAssistant?.notifications,
|
||||||
|
commentTag: issueAssistant?.commentTag,
|
||||||
|
commentPrefixTemplate: issueAssistant?.commentPrefixTemplate,
|
||||||
|
commentFooterTemplate: issueAssistant?.commentFooterTemplate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _eventSourceDocumentsMatch(
|
||||||
|
IssueAssistantEventSourceDocument left,
|
||||||
|
IssueAssistantEventSourceDocument right,
|
||||||
|
) {
|
||||||
|
return left.type == right.type &&
|
||||||
|
left.pollInterval == right.pollInterval &&
|
||||||
|
left.reconciliation == right.reconciliation &&
|
||||||
|
left.reconcileInterval == right.reconcileInterval;
|
||||||
|
}
|
||||||
|
|
||||||
class ScmConfig {
|
class ScmConfig {
|
||||||
const ScmConfig({required this.provider});
|
const ScmConfig({required this.provider});
|
||||||
|
|
||||||
|
|
@ -422,7 +446,7 @@ class IssueAssistantConfig {
|
||||||
sealed class IssueEventSourceConfig {
|
sealed class IssueEventSourceConfig {
|
||||||
const IssueEventSourceConfig({required this.type});
|
const IssueEventSourceConfig({required this.type});
|
||||||
|
|
||||||
final IssueAssistantEventSourceKind type;
|
final IssueTrackerEventSourceKind type;
|
||||||
|
|
||||||
bool get usesChannelEventSource => !_isPollingEventSource(type);
|
bool get usesChannelEventSource => !_isPollingEventSource(type);
|
||||||
}
|
}
|
||||||
|
|
@ -436,7 +460,7 @@ class PollingIssueEventSourceConfig extends IssueEventSourceConfig {
|
||||||
final Duration pollInterval;
|
final Duration pollInterval;
|
||||||
|
|
||||||
PollingIssueEventSourceConfig copyWith({
|
PollingIssueEventSourceConfig copyWith({
|
||||||
IssueAssistantEventSourceKind? type,
|
IssueTrackerEventSourceKind? type,
|
||||||
Duration? pollInterval,
|
Duration? pollInterval,
|
||||||
}) {
|
}) {
|
||||||
return PollingIssueEventSourceConfig(
|
return PollingIssueEventSourceConfig(
|
||||||
|
|
@ -453,12 +477,12 @@ class ChannelIssueEventSourceConfig extends IssueEventSourceConfig {
|
||||||
required this.reconcileInterval,
|
required this.reconcileInterval,
|
||||||
});
|
});
|
||||||
|
|
||||||
final IssueAssistantReconciliationKind reconciliation;
|
final IssueTrackerReconciliationKind reconciliation;
|
||||||
final Duration reconcileInterval;
|
final Duration reconcileInterval;
|
||||||
|
|
||||||
ChannelIssueEventSourceConfig copyWith({
|
ChannelIssueEventSourceConfig copyWith({
|
||||||
IssueAssistantEventSourceKind? type,
|
IssueTrackerEventSourceKind? type,
|
||||||
IssueAssistantReconciliationKind? reconciliation,
|
IssueTrackerReconciliationKind? reconciliation,
|
||||||
Duration? reconcileInterval,
|
Duration? reconcileInterval,
|
||||||
}) {
|
}) {
|
||||||
return ChannelIssueEventSourceConfig(
|
return ChannelIssueEventSourceConfig(
|
||||||
|
|
@ -608,7 +632,7 @@ class _IssueAssistantConfigResolver {
|
||||||
required IssueAssistantEventSourceDocument? overlay,
|
required IssueAssistantEventSourceDocument? overlay,
|
||||||
}) {
|
}) {
|
||||||
final type =
|
final type =
|
||||||
overlay?.type ?? base?.type ?? IssueAssistantEventSourceKind.ghPolling;
|
overlay?.type ?? base?.type ?? IssueTrackerEventSourceKind.ghPolling;
|
||||||
if (_isPollingEventSource(type)) {
|
if (_isPollingEventSource(type)) {
|
||||||
if (overlay?.reconciliation != null ||
|
if (overlay?.reconciliation != null ||
|
||||||
overlay?.reconcileInterval != null) {
|
overlay?.reconcileInterval != null) {
|
||||||
|
|
@ -643,7 +667,7 @@ class _IssueAssistantConfigResolver {
|
||||||
switch (base) {
|
switch (base) {
|
||||||
ChannelIssueEventSourceConfig(:final reconciliation) =>
|
ChannelIssueEventSourceConfig(:final reconciliation) =>
|
||||||
reconciliation,
|
reconciliation,
|
||||||
_ => IssueAssistantReconciliationKind.continuous,
|
_ => IssueTrackerReconciliationKind.continuous,
|
||||||
},
|
},
|
||||||
reconcileInterval: overlay?.reconcileInterval != null
|
reconcileInterval: overlay?.reconcileInterval != null
|
||||||
? _parseDuration(overlay!.reconcileInterval!)
|
? _parseDuration(overlay!.reconcileInterval!)
|
||||||
|
|
@ -654,27 +678,27 @@ class _IssueAssistantConfigResolver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _eventSourceConfigValue(IssueAssistantEventSourceKind value) {
|
String _eventSourceConfigValue(IssueTrackerEventSourceKind value) {
|
||||||
return switch (value) {
|
return switch (value) {
|
||||||
IssueAssistantEventSourceKind.ghPolling => 'gh/polling',
|
IssueTrackerEventSourceKind.ghPolling => 'gh/polling',
|
||||||
IssueAssistantEventSourceKind.glabPolling => 'glab/polling',
|
IssueTrackerEventSourceKind.glabPolling => 'glab/polling',
|
||||||
IssueAssistantEventSourceKind.teaPolling => 'tea/polling',
|
IssueTrackerEventSourceKind.teaPolling => 'tea/polling',
|
||||||
IssueAssistantEventSourceKind.opPolling => 'op/polling',
|
IssueTrackerEventSourceKind.opPolling => 'op/polling',
|
||||||
IssueAssistantEventSourceKind.ghGosmee => 'gh/gosmee',
|
IssueTrackerEventSourceKind.ghGosmee => 'gh/gosmee',
|
||||||
IssueAssistantEventSourceKind.ghWebhookForward => 'gh/webhook-forward',
|
IssueTrackerEventSourceKind.ghWebhookForward => 'gh/webhook-forward',
|
||||||
IssueAssistantEventSourceKind.teaGosmee => 'tea/gosmee',
|
IssueTrackerEventSourceKind.teaGosmee => 'tea/gosmee',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isPollingEventSource(IssueAssistantEventSourceKind value) {
|
bool _isPollingEventSource(IssueTrackerEventSourceKind value) {
|
||||||
return switch (value) {
|
return switch (value) {
|
||||||
IssueAssistantEventSourceKind.ghPolling => true,
|
IssueTrackerEventSourceKind.ghPolling => true,
|
||||||
IssueAssistantEventSourceKind.glabPolling => true,
|
IssueTrackerEventSourceKind.glabPolling => true,
|
||||||
IssueAssistantEventSourceKind.teaPolling => true,
|
IssueTrackerEventSourceKind.teaPolling => true,
|
||||||
IssueAssistantEventSourceKind.opPolling => true,
|
IssueTrackerEventSourceKind.opPolling => true,
|
||||||
IssueAssistantEventSourceKind.ghGosmee => false,
|
IssueTrackerEventSourceKind.ghGosmee => false,
|
||||||
IssueAssistantEventSourceKind.ghWebhookForward => false,
|
IssueTrackerEventSourceKind.ghWebhookForward => false,
|
||||||
IssueAssistantEventSourceKind.teaGosmee => false,
|
IssueTrackerEventSourceKind.teaGosmee => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -864,6 +888,13 @@ void _assertCamelCaseConfigKeys(Map<String, dynamic> root) {
|
||||||
issueTracker,
|
issueTracker,
|
||||||
scope: 'projects.${entry.key}.issueTracker',
|
scope: 'projects.${entry.key}.issueTracker',
|
||||||
);
|
);
|
||||||
|
final issueTrackerEventSource = issueTracker['eventSource'];
|
||||||
|
if (issueTrackerEventSource is Map<String, dynamic>) {
|
||||||
|
_assertCamelCaseMap(
|
||||||
|
issueTrackerEventSource,
|
||||||
|
scope: 'projects.${entry.key}.issueTracker.eventSource',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_assertCamelCaseMap(project, scope: 'projects.${entry.key}');
|
_assertCamelCaseMap(project, scope: 'projects.${entry.key}');
|
||||||
final scm = project['scm'];
|
final scm = project['scm'];
|
||||||
|
|
@ -919,6 +950,20 @@ ScmProvider? _defaultScmProviderForTracker(IssueTrackerProvider provider) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IssueTrackerProvider _issueTrackerProviderForEventSource(
|
||||||
|
IssueTrackerEventSourceKind type,
|
||||||
|
) {
|
||||||
|
return switch (type) {
|
||||||
|
IssueTrackerEventSourceKind.ghPolling => IssueTrackerProvider.github,
|
||||||
|
IssueTrackerEventSourceKind.glabPolling => IssueTrackerProvider.gitlab,
|
||||||
|
IssueTrackerEventSourceKind.teaPolling => IssueTrackerProvider.gitea,
|
||||||
|
IssueTrackerEventSourceKind.opPolling => IssueTrackerProvider.openproject,
|
||||||
|
IssueTrackerEventSourceKind.ghGosmee => IssueTrackerProvider.github,
|
||||||
|
IssueTrackerEventSourceKind.ghWebhookForward => IssueTrackerProvider.github,
|
||||||
|
IssueTrackerEventSourceKind.teaGosmee => IssueTrackerProvider.gitea,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
void _normalizeLegacyEventSourceConfig(Map<String, dynamic> root) {
|
void _normalizeLegacyEventSourceConfig(Map<String, dynamic> root) {
|
||||||
void normalizeProjectTracker(Map<String, dynamic> project) {
|
void normalizeProjectTracker(Map<String, dynamic> project) {
|
||||||
final repo = project['repo'];
|
final repo = project['repo'];
|
||||||
|
|
@ -942,9 +987,31 @@ void _normalizeLegacyEventSourceConfig(Map<String, dynamic> root) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void normalizeIssueTrackerEventSource(
|
||||||
|
Map<String, dynamic> issueTracker, {
|
||||||
|
required IssueTrackerEventSourceKind? defaultType,
|
||||||
|
}) {
|
||||||
|
final rawEventSource = issueTracker['eventSource'];
|
||||||
|
Map<String, dynamic>? normalizedEventSource;
|
||||||
|
|
||||||
|
if (rawEventSource is String) {
|
||||||
|
normalizedEventSource = <String, dynamic>{'type': rawEventSource};
|
||||||
|
} else if (rawEventSource is Map<String, dynamic>) {
|
||||||
|
normalizedEventSource = Map<String, dynamic>.from(rawEventSource);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultType != null && !normalizedEventSource.containsKey('type')) {
|
||||||
|
normalizedEventSource['type'] = _eventSourceConfigValue(defaultType);
|
||||||
|
}
|
||||||
|
|
||||||
|
issueTracker['eventSource'] = normalizedEventSource;
|
||||||
|
}
|
||||||
|
|
||||||
void normalizeIssueAssistant(
|
void normalizeIssueAssistant(
|
||||||
Map<String, dynamic> issueAssistant, {
|
Map<String, dynamic> issueAssistant, {
|
||||||
required IssueAssistantEventSourceKind? defaultType,
|
required IssueTrackerEventSourceKind? defaultType,
|
||||||
}) {
|
}) {
|
||||||
final rawEventSource = issueAssistant['eventSource'];
|
final rawEventSource = issueAssistant['eventSource'];
|
||||||
Map<String, dynamic>? normalizedEventSource;
|
Map<String, dynamic>? normalizedEventSource;
|
||||||
|
|
@ -989,7 +1056,7 @@ void _normalizeLegacyEventSourceConfig(Map<String, dynamic> root) {
|
||||||
if (issueAssistant is Map<String, dynamic>) {
|
if (issueAssistant is Map<String, dynamic>) {
|
||||||
normalizeIssueAssistant(
|
normalizeIssueAssistant(
|
||||||
issueAssistant,
|
issueAssistant,
|
||||||
defaultType: IssueAssistantEventSourceKind.ghPolling,
|
defaultType: IssueTrackerEventSourceKind.ghPolling,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1002,8 +1069,28 @@ void _normalizeLegacyEventSourceConfig(Map<String, dynamic> root) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
normalizeProjectTracker(project);
|
normalizeProjectTracker(project);
|
||||||
|
final issueTracker = project['issueTracker'];
|
||||||
|
if (issueTracker is Map<String, dynamic>) {
|
||||||
|
normalizeIssueTrackerEventSource(
|
||||||
|
issueTracker,
|
||||||
|
defaultType: switch (issueTracker['provider'] as String?) {
|
||||||
|
'gitlab' => IssueTrackerEventSourceKind.glabPolling,
|
||||||
|
'gitea' => IssueTrackerEventSourceKind.teaPolling,
|
||||||
|
'openproject' => IssueTrackerEventSourceKind.opPolling,
|
||||||
|
_ => IssueTrackerEventSourceKind.ghPolling,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
final issueAssistant = project['issueAssistant'];
|
final issueAssistant = project['issueAssistant'];
|
||||||
if (issueAssistant is! Map<String, dynamic>) {
|
if (issueAssistant is! Map<String, dynamic>) {
|
||||||
|
if (issueTracker is Map<String, dynamic> &&
|
||||||
|
issueTracker['eventSource'] is Map<String, dynamic>) {
|
||||||
|
project['issueAssistant'] = <String, dynamic>{
|
||||||
|
'eventSource': Map<String, dynamic>.from(
|
||||||
|
issueTracker['eventSource'] as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
normalizeIssueAssistant(
|
normalizeIssueAssistant(
|
||||||
|
|
@ -1011,11 +1098,19 @@ void _normalizeLegacyEventSourceConfig(Map<String, dynamic> root) {
|
||||||
defaultType: switch ((project['issueTracker']
|
defaultType: switch ((project['issueTracker']
|
||||||
as Map<String, dynamic>?)?['provider']
|
as Map<String, dynamic>?)?['provider']
|
||||||
as String?) {
|
as String?) {
|
||||||
'gitlab' => IssueAssistantEventSourceKind.glabPolling,
|
'gitlab' => IssueTrackerEventSourceKind.glabPolling,
|
||||||
'gitea' => IssueAssistantEventSourceKind.teaPolling,
|
'gitea' => IssueTrackerEventSourceKind.teaPolling,
|
||||||
_ => IssueAssistantEventSourceKind.ghPolling,
|
'openproject' => IssueTrackerEventSourceKind.opPolling,
|
||||||
|
_ => IssueTrackerEventSourceKind.ghPolling,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
if (issueTracker is Map<String, dynamic> &&
|
||||||
|
issueTracker['eventSource'] == null &&
|
||||||
|
issueAssistant['eventSource'] is Map<String, dynamic>) {
|
||||||
|
issueTracker['eventSource'] = Map<String, dynamic>.from(
|
||||||
|
issueAssistant['eventSource'] as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1098,10 +1193,6 @@ ${_YamlEmitter().write({'projects': projectsSection})}
|
||||||
|
|
||||||
# issueAssistant:
|
# issueAssistant:
|
||||||
# enabled: ${assistant?.enabled ?? true}
|
# enabled: ${assistant?.enabled ?? true}
|
||||||
# eventSource:
|
|
||||||
# type: gh/gosmee
|
|
||||||
# reconciliation: startup-only
|
|
||||||
# reconcileInterval: 30s
|
|
||||||
# mentionTriggers: ["@helper"]
|
# mentionTriggers: ["@helper"]
|
||||||
# sessionPrefix: sample
|
# sessionPrefix: sample
|
||||||
'''
|
'''
|
||||||
|
|
|
||||||
|
|
@ -88,9 +88,10 @@ class ProjectConfigDocument {
|
||||||
includeIfNull: false,
|
includeIfNull: false,
|
||||||
)
|
)
|
||||||
class IssueTrackerConfigDocument {
|
class IssueTrackerConfigDocument {
|
||||||
const IssueTrackerConfigDocument({this.provider});
|
const IssueTrackerConfigDocument({this.provider, this.eventSource});
|
||||||
|
|
||||||
final IssueTrackerProvider? provider;
|
final IssueTrackerProvider? provider;
|
||||||
|
final IssueAssistantEventSourceDocument? eventSource;
|
||||||
|
|
||||||
factory IssueTrackerConfigDocument.fromJson(Map<String, dynamic> json) =>
|
factory IssueTrackerConfigDocument.fromJson(Map<String, dynamic> json) =>
|
||||||
_$IssueTrackerConfigDocumentFromJson(json);
|
_$IssueTrackerConfigDocumentFromJson(json);
|
||||||
|
|
@ -171,9 +172,9 @@ class IssueAssistantEventSourceDocument {
|
||||||
this.reconcileInterval,
|
this.reconcileInterval,
|
||||||
});
|
});
|
||||||
|
|
||||||
final IssueAssistantEventSourceKind? type;
|
final IssueTrackerEventSourceKind? type;
|
||||||
final String? pollInterval;
|
final String? pollInterval;
|
||||||
final IssueAssistantReconciliationKind? reconciliation;
|
final IssueTrackerReconciliationKind? reconciliation;
|
||||||
final String? reconcileInterval;
|
final String? reconcileInterval;
|
||||||
|
|
||||||
factory IssueAssistantEventSourceDocument.fromJson(
|
factory IssueAssistantEventSourceDocument.fromJson(
|
||||||
|
|
@ -247,7 +248,7 @@ class NotificationConfigDocument {
|
||||||
@JsonEnum(fieldRename: FieldRename.snake)
|
@JsonEnum(fieldRename: FieldRename.snake)
|
||||||
enum AssistantCapability { planning, bugVerification }
|
enum AssistantCapability { planning, bugVerification }
|
||||||
|
|
||||||
enum IssueAssistantEventSourceKind {
|
enum IssueTrackerEventSourceKind {
|
||||||
@JsonValue('gh/polling')
|
@JsonValue('gh/polling')
|
||||||
ghPolling,
|
ghPolling,
|
||||||
@JsonValue('glab/polling')
|
@JsonValue('glab/polling')
|
||||||
|
|
@ -264,7 +265,7 @@ enum IssueAssistantEventSourceKind {
|
||||||
teaGosmee,
|
teaGosmee,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum IssueAssistantReconciliationKind {
|
enum IssueTrackerReconciliationKind {
|
||||||
@JsonValue('startup-only')
|
@JsonValue('startup-only')
|
||||||
startupOnly,
|
startupOnly,
|
||||||
@JsonValue('continuous')
|
@JsonValue('continuous')
|
||||||
|
|
|
||||||
|
|
@ -63,21 +63,21 @@ class IssueAssistantApp {
|
||||||
issueTrackerClient: issueTrackerClient,
|
issueTrackerClient: issueTrackerClient,
|
||||||
issueEventSource: RoutedIssueEventSource(
|
issueEventSource: RoutedIssueEventSource(
|
||||||
sources: {
|
sources: {
|
||||||
IssueAssistantEventSourceKind.ghPolling: pollingIssueEventSource,
|
IssueTrackerEventSourceKind.ghPolling: pollingIssueEventSource,
|
||||||
IssueAssistantEventSourceKind.glabPolling: pollingIssueEventSource,
|
IssueTrackerEventSourceKind.glabPolling: pollingIssueEventSource,
|
||||||
IssueAssistantEventSourceKind.teaPolling: pollingIssueEventSource,
|
IssueTrackerEventSourceKind.teaPolling: pollingIssueEventSource,
|
||||||
IssueAssistantEventSourceKind.opPolling: pollingIssueEventSource,
|
IssueTrackerEventSourceKind.opPolling: pollingIssueEventSource,
|
||||||
IssueAssistantEventSourceKind.ghGosmee: GitHubGosmeeIssueEventSource(
|
IssueTrackerEventSourceKind.ghGosmee: GitHubGosmeeIssueEventSource(
|
||||||
issueTrackerClient: issueTrackerClient,
|
issueTrackerClient: issueTrackerClient,
|
||||||
gosmeeCommand: gosmeeCommand,
|
gosmeeCommand: gosmeeCommand,
|
||||||
logger: _logger,
|
logger: _logger,
|
||||||
),
|
),
|
||||||
IssueAssistantEventSourceKind.ghWebhookForward:
|
IssueTrackerEventSourceKind.ghWebhookForward:
|
||||||
GitHubWebhookForwardIssueEventSource(
|
GitHubWebhookForwardIssueEventSource(
|
||||||
issueTrackerClient: issueTrackerClient,
|
issueTrackerClient: issueTrackerClient,
|
||||||
logger: _logger,
|
logger: _logger,
|
||||||
),
|
),
|
||||||
IssueAssistantEventSourceKind.teaGosmee: GiteaGosmeeIssueEventSource(
|
IssueTrackerEventSourceKind.teaGosmee: GiteaGosmeeIssueEventSource(
|
||||||
issueTrackerClient: issueTrackerClient,
|
issueTrackerClient: issueTrackerClient,
|
||||||
gosmeeCommand: gosmeeCommand,
|
gosmeeCommand: gosmeeCommand,
|
||||||
logger: _logger,
|
logger: _logger,
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ abstract class IssueEventSource {
|
||||||
class RoutedIssueEventSource implements IssueEventSource {
|
class RoutedIssueEventSource implements IssueEventSource {
|
||||||
RoutedIssueEventSource({required this.sources});
|
RoutedIssueEventSource({required this.sources});
|
||||||
|
|
||||||
final Map<IssueAssistantEventSourceKind, IssueEventSource> sources;
|
final Map<IssueTrackerEventSourceKind, IssueEventSource> sources;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> run({
|
Future<void> run({
|
||||||
|
|
@ -50,17 +50,15 @@ class RoutedIssueEventSource implements IssueEventSource {
|
||||||
.where(_shouldRunStartupReconciliation)
|
.where(_shouldRunStartupReconciliation)
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
if (startupReconcileProjects.isNotEmpty) {
|
if (startupReconcileProjects.isNotEmpty) {
|
||||||
await sources[IssueAssistantEventSourceKind.ghPolling]!.runOnce(
|
await sources[IssueTrackerEventSourceKind.ghPolling]!.runOnce(
|
||||||
projects: startupReconcileProjects,
|
projects: startupReconcileProjects,
|
||||||
onBatch: onBatch,
|
onBatch: onBatch,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final projectsBySource =
|
final projectsBySource = <IssueTrackerEventSourceKind, List<ProjectConfig>>{
|
||||||
<IssueAssistantEventSourceKind, List<ProjectConfig>>{
|
for (final eventSource in sources.keys) eventSource: <ProjectConfig>[],
|
||||||
for (final eventSource in sources.keys)
|
};
|
||||||
eventSource: <ProjectConfig>[],
|
|
||||||
};
|
|
||||||
for (final project in projects) {
|
for (final project in projects) {
|
||||||
for (final projectSource in _registeredSourcesFor(project)) {
|
for (final projectSource in _registeredSourcesFor(project)) {
|
||||||
final sourceProjects = projectsBySource[projectSource];
|
final sourceProjects = projectsBySource[projectSource];
|
||||||
|
|
@ -90,7 +88,7 @@ class RoutedIssueEventSource implements IssueEventSource {
|
||||||
required List<ProjectConfig> projects,
|
required List<ProjectConfig> projects,
|
||||||
required IssueEventBatchHandler onBatch,
|
required IssueEventBatchHandler onBatch,
|
||||||
}) {
|
}) {
|
||||||
return sources[IssueAssistantEventSourceKind.ghPolling]!.runOnce(
|
return sources[IssueTrackerEventSourceKind.ghPolling]!.runOnce(
|
||||||
projects: projects,
|
projects: projects,
|
||||||
onBatch: onBatch,
|
onBatch: onBatch,
|
||||||
);
|
);
|
||||||
|
|
@ -103,33 +101,33 @@ class RoutedIssueEventSource implements IssueEventSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Iterable<IssueAssistantEventSourceKind> _registeredSourcesFor(
|
Iterable<IssueTrackerEventSourceKind> _registeredSourcesFor(
|
||||||
ProjectConfig project,
|
ProjectConfig project,
|
||||||
) sync* {
|
) sync* {
|
||||||
switch (project.issueAssistant.eventSource.type) {
|
switch (project.issueAssistant.eventSource.type) {
|
||||||
case IssueAssistantEventSourceKind.ghPolling:
|
case IssueTrackerEventSourceKind.ghPolling:
|
||||||
yield IssueAssistantEventSourceKind.ghPolling;
|
yield IssueTrackerEventSourceKind.ghPolling;
|
||||||
case IssueAssistantEventSourceKind.glabPolling:
|
case IssueTrackerEventSourceKind.glabPolling:
|
||||||
yield IssueAssistantEventSourceKind.glabPolling;
|
yield IssueTrackerEventSourceKind.glabPolling;
|
||||||
case IssueAssistantEventSourceKind.teaPolling:
|
case IssueTrackerEventSourceKind.teaPolling:
|
||||||
yield IssueAssistantEventSourceKind.teaPolling;
|
yield IssueTrackerEventSourceKind.teaPolling;
|
||||||
case IssueAssistantEventSourceKind.opPolling:
|
case IssueTrackerEventSourceKind.opPolling:
|
||||||
yield IssueAssistantEventSourceKind.opPolling;
|
yield IssueTrackerEventSourceKind.opPolling;
|
||||||
case IssueAssistantEventSourceKind.ghGosmee:
|
case IssueTrackerEventSourceKind.ghGosmee:
|
||||||
if (_shouldRunContinuousReconciliation(project)) {
|
if (_shouldRunContinuousReconciliation(project)) {
|
||||||
yield IssueAssistantEventSourceKind.ghPolling;
|
yield IssueTrackerEventSourceKind.ghPolling;
|
||||||
}
|
}
|
||||||
yield IssueAssistantEventSourceKind.ghGosmee;
|
yield IssueTrackerEventSourceKind.ghGosmee;
|
||||||
case IssueAssistantEventSourceKind.ghWebhookForward:
|
case IssueTrackerEventSourceKind.ghWebhookForward:
|
||||||
if (_shouldRunContinuousReconciliation(project)) {
|
if (_shouldRunContinuousReconciliation(project)) {
|
||||||
yield IssueAssistantEventSourceKind.ghPolling;
|
yield IssueTrackerEventSourceKind.ghPolling;
|
||||||
}
|
}
|
||||||
yield IssueAssistantEventSourceKind.ghWebhookForward;
|
yield IssueTrackerEventSourceKind.ghWebhookForward;
|
||||||
case IssueAssistantEventSourceKind.teaGosmee:
|
case IssueTrackerEventSourceKind.teaGosmee:
|
||||||
if (_shouldRunContinuousReconciliation(project)) {
|
if (_shouldRunContinuousReconciliation(project)) {
|
||||||
yield IssueAssistantEventSourceKind.teaPolling;
|
yield IssueTrackerEventSourceKind.teaPolling;
|
||||||
}
|
}
|
||||||
yield IssueAssistantEventSourceKind.teaGosmee;
|
yield IssueTrackerEventSourceKind.teaGosmee;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,7 +139,7 @@ class RoutedIssueEventSource implements IssueEventSource {
|
||||||
project.issueAssistant.eventSource is ChannelIssueEventSourceConfig &&
|
project.issueAssistant.eventSource is ChannelIssueEventSourceConfig &&
|
||||||
(project.issueAssistant.eventSource as ChannelIssueEventSourceConfig)
|
(project.issueAssistant.eventSource as ChannelIssueEventSourceConfig)
|
||||||
.reconciliation ==
|
.reconciliation ==
|
||||||
IssueAssistantReconciliationKind.continuous;
|
IssueTrackerReconciliationKind.continuous;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PollingIssueEventSource implements IssueEventSource {
|
class PollingIssueEventSource implements IssueEventSource {
|
||||||
|
|
|
||||||
|
|
@ -407,21 +407,22 @@ projects:
|
||||||
});
|
});
|
||||||
|
|
||||||
/// ```gherkin
|
/// ```gherkin
|
||||||
/// Scenario: Load an explicit Gitea issue tracker provider
|
/// Scenario: Infer a Gitea issue tracker provider from the event source
|
||||||
/// Given a config whose project sets issueTracker.provider to gitea
|
/// Given a config whose project sets tea/polling as the event source type
|
||||||
/// When loading the app config from disk
|
/// When loading the app config from disk
|
||||||
/// Then the project keeps the configured Gitea provider
|
/// Then the project infers the Gitea provider
|
||||||
/// And the repository slug still parses normally
|
/// And the repository slug still parses normally
|
||||||
/// ```
|
/// ```
|
||||||
test('Load an explicit Gitea issue tracker provider', () async {
|
test('Infer a Gitea issue tracker provider from the event source', () async {
|
||||||
// Given a config whose project sets issueTracker.provider to gitea.
|
// Given a config whose project sets tea/polling as the event source type.
|
||||||
final tempDir = await Directory.systemTemp.createTemp('cws-gitea-');
|
final tempDir = await Directory.systemTemp.createTemp('cws-gitea-');
|
||||||
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
|
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
|
||||||
await configFile.writeAsString('''
|
await configFile.writeAsString('''
|
||||||
projects:
|
projects:
|
||||||
sample:
|
sample:
|
||||||
issueTracker:
|
issueTracker:
|
||||||
provider: gitea
|
eventSource:
|
||||||
|
type: tea/polling
|
||||||
repo: owner/sample
|
repo: owner/sample
|
||||||
path: ${tempDir.path}
|
path: ${tempDir.path}
|
||||||
''');
|
''');
|
||||||
|
|
@ -429,7 +430,7 @@ projects:
|
||||||
// When loading the app config from disk.
|
// When loading the app config from disk.
|
||||||
final config = await AppConfig.load(configFile.path);
|
final config = await AppConfig.load(configFile.path);
|
||||||
|
|
||||||
// Then the project keeps the configured Gitea provider.
|
// Then the project infers the Gitea provider.
|
||||||
expect(config.projects['sample']!.provider, IssueTrackerProvider.gitea);
|
expect(config.projects['sample']!.provider, IssueTrackerProvider.gitea);
|
||||||
expect(config.projects['sample']!.scm.provider, ScmProvider.gitea);
|
expect(config.projects['sample']!.scm.provider, ScmProvider.gitea);
|
||||||
|
|
||||||
|
|
@ -437,26 +438,27 @@ projects:
|
||||||
expect(config.projects['sample']!.repoSlug!.fullName, 'owner/sample');
|
expect(config.projects['sample']!.repoSlug!.fullName, 'owner/sample');
|
||||||
expect(
|
expect(
|
||||||
config.projects['sample']!.issueAssistant.eventSource.type,
|
config.projects['sample']!.issueAssistant.eventSource.type,
|
||||||
IssueAssistantEventSourceKind.teaPolling,
|
IssueTrackerEventSourceKind.teaPolling,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/// ```gherkin
|
/// ```gherkin
|
||||||
/// Scenario: Load an explicit GitLab issue tracker provider
|
/// Scenario: Infer a GitLab issue tracker provider from the event source
|
||||||
/// Given a config whose project sets issueTracker.provider to gitlab
|
/// Given a config whose project sets glab/polling as the event source type
|
||||||
/// When loading the app config from disk
|
/// When loading the app config from disk
|
||||||
/// Then the project keeps the configured GitLab provider
|
/// Then the project infers the GitLab provider
|
||||||
/// And the project uses GitLab polling and GitLab SCM by default
|
/// And the project uses GitLab polling and GitLab SCM by default
|
||||||
/// ```
|
/// ```
|
||||||
test('Load an explicit GitLab issue tracker provider', () async {
|
test('Infer a GitLab issue tracker provider from the event source', () async {
|
||||||
// Given a config whose project sets issueTracker.provider to gitlab.
|
// Given a config whose project sets glab/polling as the event source type.
|
||||||
final tempDir = await Directory.systemTemp.createTemp('cws-gitlab-');
|
final tempDir = await Directory.systemTemp.createTemp('cws-gitlab-');
|
||||||
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
|
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
|
||||||
await configFile.writeAsString('''
|
await configFile.writeAsString('''
|
||||||
projects:
|
projects:
|
||||||
sample:
|
sample:
|
||||||
issueTracker:
|
issueTracker:
|
||||||
provider: gitlab
|
eventSource:
|
||||||
|
type: glab/polling
|
||||||
repo: group/subgroup/sample
|
repo: group/subgroup/sample
|
||||||
path: ${tempDir.path}
|
path: ${tempDir.path}
|
||||||
''');
|
''');
|
||||||
|
|
@ -464,67 +466,75 @@ projects:
|
||||||
// When loading the app config from disk.
|
// When loading the app config from disk.
|
||||||
final config = await AppConfig.load(configFile.path);
|
final config = await AppConfig.load(configFile.path);
|
||||||
|
|
||||||
// Then the project keeps the configured GitLab provider.
|
// Then the project infers the GitLab provider.
|
||||||
expect(config.projects['sample']!.provider, IssueTrackerProvider.gitlab);
|
expect(config.projects['sample']!.provider, IssueTrackerProvider.gitlab);
|
||||||
|
|
||||||
// And the project uses GitLab polling and GitLab SCM by default.
|
// And the project uses GitLab polling and GitLab SCM by default.
|
||||||
expect(config.projects['sample']!.scm.provider, ScmProvider.gitlab);
|
expect(config.projects['sample']!.scm.provider, ScmProvider.gitlab);
|
||||||
expect(
|
expect(
|
||||||
config.projects['sample']!.issueAssistant.eventSource.type,
|
config.projects['sample']!.issueAssistant.eventSource.type,
|
||||||
IssueAssistantEventSourceKind.glabPolling,
|
IssueTrackerEventSourceKind.glabPolling,
|
||||||
);
|
);
|
||||||
expect(config.projects['sample']!.repo, 'group/subgroup/sample');
|
expect(config.projects['sample']!.repo, 'group/subgroup/sample');
|
||||||
});
|
});
|
||||||
|
|
||||||
/// ```gherkin
|
/// ```gherkin
|
||||||
/// Scenario: Load an explicit OpenProject issue tracker provider
|
/// Scenario: Infer an OpenProject issue tracker provider from the event source
|
||||||
/// Given a config whose project sets issueTracker.provider to openproject
|
/// Given a config whose project sets op/polling as the event source type
|
||||||
/// And the project declares an explicit SCM provider
|
/// And the project declares an explicit SCM provider
|
||||||
/// When loading the app config from disk
|
/// When loading the app config from disk
|
||||||
/// Then the project keeps the configured OpenProject provider
|
/// Then the project infers the OpenProject provider
|
||||||
/// And the project uses op/polling as its default issue event source
|
/// And the project uses op/polling as its default issue event source
|
||||||
/// ```
|
/// ```
|
||||||
test('Load an explicit OpenProject issue tracker provider', () async {
|
test(
|
||||||
// Given a config whose project sets issueTracker.provider to openproject.
|
'Infer an OpenProject issue tracker provider from the event source',
|
||||||
final tempDir = await Directory.systemTemp.createTemp('cws-openproject-');
|
() async {
|
||||||
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
|
// Given a config whose project sets op/polling as the event source type.
|
||||||
await configFile.writeAsString('''
|
final tempDir = await Directory.systemTemp.createTemp(
|
||||||
|
'cws-openproject-',
|
||||||
|
);
|
||||||
|
final configFile = File(
|
||||||
|
p.join(tempDir.path, 'agent-orchestrator.yaml'),
|
||||||
|
);
|
||||||
|
await configFile.writeAsString('''
|
||||||
projects:
|
projects:
|
||||||
sample:
|
sample:
|
||||||
issueTracker:
|
issueTracker:
|
||||||
provider: openproject
|
eventSource:
|
||||||
|
type: op/polling
|
||||||
scm:
|
scm:
|
||||||
provider: github
|
provider: github
|
||||||
repo: 42
|
repo: 42
|
||||||
path: ${tempDir.path}
|
path: ${tempDir.path}
|
||||||
''');
|
''');
|
||||||
|
|
||||||
// When loading the app config from disk.
|
// When loading the app config from disk.
|
||||||
final config = await AppConfig.load(configFile.path);
|
final config = await AppConfig.load(configFile.path);
|
||||||
|
|
||||||
// Then the project keeps the configured OpenProject provider.
|
// Then the project infers the OpenProject provider.
|
||||||
expect(
|
expect(
|
||||||
config.projects['sample']!.provider,
|
config.projects['sample']!.provider,
|
||||||
IssueTrackerProvider.openproject,
|
IssueTrackerProvider.openproject,
|
||||||
);
|
);
|
||||||
expect(config.projects['sample']!.scm.provider, ScmProvider.github);
|
expect(config.projects['sample']!.scm.provider, ScmProvider.github);
|
||||||
|
|
||||||
// And the project uses op/polling as its default issue event source.
|
// And the project uses op/polling as its default issue event source.
|
||||||
expect(
|
expect(
|
||||||
config.projects['sample']!.issueAssistant.eventSource.type,
|
config.projects['sample']!.issueAssistant.eventSource.type,
|
||||||
IssueAssistantEventSourceKind.opPolling,
|
IssueTrackerEventSourceKind.opPolling,
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
/// ```gherkin
|
/// ```gherkin
|
||||||
/// Scenario: Require an explicit SCM provider for OpenProject projects
|
/// Scenario: Require an explicit SCM provider for OpenProject projects
|
||||||
/// Given a config whose project uses the OpenProject issue tracker
|
/// Given a config whose project uses op/polling as the event source
|
||||||
/// And the project omits scm.provider
|
/// And the project omits scm.provider
|
||||||
/// When loading the app config from disk
|
/// When loading the app config from disk
|
||||||
/// Then loading fails with a clear SCM requirement error
|
/// Then loading fails with a clear SCM requirement error
|
||||||
/// ```
|
/// ```
|
||||||
test('Require an explicit SCM provider for OpenProject projects', () async {
|
test('Require an explicit SCM provider for OpenProject projects', () async {
|
||||||
// Given a config whose project uses the OpenProject issue tracker.
|
// Given a config whose project uses op/polling as the event source.
|
||||||
final tempDir = await Directory.systemTemp.createTemp(
|
final tempDir = await Directory.systemTemp.createTemp(
|
||||||
'cws-openproject-missing-scm-',
|
'cws-openproject-missing-scm-',
|
||||||
);
|
);
|
||||||
|
|
@ -533,7 +543,8 @@ projects:
|
||||||
projects:
|
projects:
|
||||||
sample:
|
sample:
|
||||||
issueTracker:
|
issueTracker:
|
||||||
provider: openproject
|
eventSource:
|
||||||
|
type: op/polling
|
||||||
repo: 42
|
repo: 42
|
||||||
path: ${tempDir.path}
|
path: ${tempDir.path}
|
||||||
''');
|
''');
|
||||||
|
|
@ -551,7 +562,7 @@ projects:
|
||||||
(error) => error.message,
|
(error) => error.message,
|
||||||
'message',
|
'message',
|
||||||
contains(
|
contains(
|
||||||
'projects.sample.scm.provider is required for issueTracker.provider: openproject',
|
'projects.sample.scm.provider is required for issue tracking provider: openproject',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -560,13 +571,13 @@ projects:
|
||||||
|
|
||||||
/// ```gherkin
|
/// ```gherkin
|
||||||
/// Scenario: Accept an OpenProject project name reference
|
/// Scenario: Accept an OpenProject project name reference
|
||||||
/// Given a config whose project uses the OpenProject issue tracker
|
/// Given a config whose project uses op/polling as the event source
|
||||||
/// And the project repo field is a human-readable project name
|
/// And the project repo field is a human-readable project name
|
||||||
/// When loading the app config from disk
|
/// When loading the app config from disk
|
||||||
/// Then loading succeeds without requiring a numeric project id
|
/// Then loading succeeds without requiring a numeric project id
|
||||||
/// ```
|
/// ```
|
||||||
test('Accept an OpenProject project name reference', () async {
|
test('Accept an OpenProject project name reference', () async {
|
||||||
// Given a config whose project uses the OpenProject issue tracker.
|
// Given a config whose project uses op/polling as the event source.
|
||||||
final tempDir = await Directory.systemTemp.createTemp(
|
final tempDir = await Directory.systemTemp.createTemp(
|
||||||
'cws-openproject-project-name-',
|
'cws-openproject-project-name-',
|
||||||
);
|
);
|
||||||
|
|
@ -575,7 +586,8 @@ projects:
|
||||||
projects:
|
projects:
|
||||||
sample:
|
sample:
|
||||||
issueTracker:
|
issueTracker:
|
||||||
provider: openproject
|
eventSource:
|
||||||
|
type: op/polling
|
||||||
scm:
|
scm:
|
||||||
provider: github
|
provider: github
|
||||||
repo: Demo project
|
repo: Demo project
|
||||||
|
|
@ -610,12 +622,10 @@ projects:
|
||||||
projects:
|
projects:
|
||||||
sample:
|
sample:
|
||||||
issueTracker:
|
issueTracker:
|
||||||
provider: github
|
|
||||||
repo: owner/sample
|
|
||||||
path: ${tempDir.path}
|
|
||||||
issueAssistant:
|
|
||||||
eventSource:
|
eventSource:
|
||||||
type: gh/gosmee
|
type: gh/gosmee
|
||||||
|
repo: owner/sample
|
||||||
|
path: ${tempDir.path}
|
||||||
''');
|
''');
|
||||||
|
|
||||||
// When loading the app config from disk.
|
// When loading the app config from disk.
|
||||||
|
|
@ -624,7 +634,7 @@ projects:
|
||||||
// Then the project keeps gh/gosmee as its event source.
|
// Then the project keeps gh/gosmee as its event source.
|
||||||
expect(
|
expect(
|
||||||
config.projects['sample']!.issueAssistant.eventSource.type,
|
config.projects['sample']!.issueAssistant.eventSource.type,
|
||||||
IssueAssistantEventSourceKind.ghGosmee,
|
IssueTrackerEventSourceKind.ghGosmee,
|
||||||
);
|
);
|
||||||
|
|
||||||
// And the GitHub provider remains valid for that project.
|
// And the GitHub provider remains valid for that project.
|
||||||
|
|
@ -652,7 +662,8 @@ projects:
|
||||||
projects:
|
projects:
|
||||||
sample:
|
sample:
|
||||||
issueTracker:
|
issueTracker:
|
||||||
provider: gitea
|
eventSource:
|
||||||
|
type: tea/polling
|
||||||
scm:
|
scm:
|
||||||
provider: github
|
provider: github
|
||||||
repo: owner/sample
|
repo: owner/sample
|
||||||
|
|
@ -671,15 +682,17 @@ projects:
|
||||||
);
|
);
|
||||||
|
|
||||||
/// ```gherkin
|
/// ```gherkin
|
||||||
/// Scenario: Reject gh/gosmee for non-GitHub providers
|
/// Scenario: Keep the legacy issue tracker provider as a fallback
|
||||||
/// Given a Gitea project config that opts into gh/gosmee
|
/// Given a config whose project still sets issueTracker.provider to gitea
|
||||||
|
/// And the project omits issueAssistant.eventSource.type
|
||||||
/// When loading the app config from disk
|
/// When loading the app config from disk
|
||||||
/// Then loading fails with a clear provider compatibility error
|
/// Then the project keeps the configured Gitea provider
|
||||||
|
/// And the polling event source is rewritten to tea/polling
|
||||||
/// ```
|
/// ```
|
||||||
test('Reject gh/gosmee for non-GitHub providers', () async {
|
test('Keep the legacy issue tracker provider as a fallback', () async {
|
||||||
// Given a Gitea project config that opts into gh/gosmee.
|
// Given a config whose project still sets issueTracker.provider to gitea.
|
||||||
final tempDir = await Directory.systemTemp.createTemp(
|
final tempDir = await Directory.systemTemp.createTemp(
|
||||||
'cws-gh-gosmee-gitea-',
|
'cws-legacy-provider-fallback-',
|
||||||
);
|
);
|
||||||
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
|
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
|
||||||
await configFile.writeAsString('''
|
await configFile.writeAsString('''
|
||||||
|
|
@ -689,22 +702,60 @@ projects:
|
||||||
provider: gitea
|
provider: gitea
|
||||||
repo: owner/sample
|
repo: owner/sample
|
||||||
path: ${tempDir.path}
|
path: ${tempDir.path}
|
||||||
issueAssistant:
|
''');
|
||||||
|
|
||||||
|
// And the project omits issueAssistant.eventSource.type.
|
||||||
|
|
||||||
|
// When loading the app config from disk.
|
||||||
|
final config = await AppConfig.load(configFile.path);
|
||||||
|
|
||||||
|
// Then the project keeps the configured Gitea provider.
|
||||||
|
expect(config.projects['sample']!.provider, IssueTrackerProvider.gitea);
|
||||||
|
|
||||||
|
// And the polling event source is rewritten to tea/polling.
|
||||||
|
expect(
|
||||||
|
config.projects['sample']!.issueAssistant.eventSource.type,
|
||||||
|
IssueTrackerEventSourceKind.teaPolling,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/// ```gherkin
|
||||||
|
/// Scenario: Reject conflicting legacy tracker and event source settings
|
||||||
|
/// Given a project config whose legacy tracker provider is gitea
|
||||||
|
/// And the project opts into gh/gosmee
|
||||||
|
/// When loading the app config from disk
|
||||||
|
/// Then loading fails with a clear conflict error
|
||||||
|
/// ```
|
||||||
|
test('Reject conflicting legacy tracker and event source settings', () async {
|
||||||
|
// Given a project config whose legacy tracker provider is gitea.
|
||||||
|
final tempDir = await Directory.systemTemp.createTemp(
|
||||||
|
'cws-gh-gosmee-gitea-',
|
||||||
|
);
|
||||||
|
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
|
||||||
|
await configFile.writeAsString('''
|
||||||
|
projects:
|
||||||
|
sample:
|
||||||
|
issueTracker:
|
||||||
|
provider: gitea
|
||||||
eventSource:
|
eventSource:
|
||||||
type: gh/gosmee
|
type: gh/gosmee
|
||||||
|
repo: owner/sample
|
||||||
|
path: ${tempDir.path}
|
||||||
''');
|
''');
|
||||||
|
|
||||||
// When loading the app config from disk.
|
// When loading the app config from disk.
|
||||||
final load = AppConfig.load(configFile.path);
|
final load = AppConfig.load(configFile.path);
|
||||||
|
|
||||||
// Then loading fails with a clear provider compatibility error.
|
// Then loading fails with a clear conflict error.
|
||||||
await expectLater(
|
await expectLater(
|
||||||
load,
|
load,
|
||||||
throwsA(
|
throwsA(
|
||||||
isA<FormatException>().having(
|
isA<FormatException>().having(
|
||||||
(error) => error.message,
|
(error) => error.message,
|
||||||
'message',
|
'message',
|
||||||
contains('gh/gosmee requires issueTracker.provider: github'),
|
contains(
|
||||||
|
'gh/gosmee implies issue tracker provider github, but the legacy configured provider is gitea',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -727,12 +778,10 @@ projects:
|
||||||
projects:
|
projects:
|
||||||
sample:
|
sample:
|
||||||
issueTracker:
|
issueTracker:
|
||||||
provider: github
|
|
||||||
repo: owner/sample
|
|
||||||
path: ${tempDir.path}
|
|
||||||
issueAssistant:
|
|
||||||
eventSource:
|
eventSource:
|
||||||
type: gh/webhook-forward
|
type: gh/webhook-forward
|
||||||
|
repo: owner/sample
|
||||||
|
path: ${tempDir.path}
|
||||||
''');
|
''');
|
||||||
|
|
||||||
// When loading the app config from disk.
|
// When loading the app config from disk.
|
||||||
|
|
@ -741,7 +790,7 @@ projects:
|
||||||
// Then the project keeps gh/webhook-forward as its event source.
|
// Then the project keeps gh/webhook-forward as its event source.
|
||||||
expect(
|
expect(
|
||||||
config.projects['sample']!.issueAssistant.eventSource.type,
|
config.projects['sample']!.issueAssistant.eventSource.type,
|
||||||
IssueAssistantEventSourceKind.ghWebhookForward,
|
IssueTrackerEventSourceKind.ghWebhookForward,
|
||||||
);
|
);
|
||||||
|
|
||||||
// And the GitHub provider remains valid for that project.
|
// And the GitHub provider remains valid for that project.
|
||||||
|
|
@ -765,14 +814,12 @@ projects:
|
||||||
projects:
|
projects:
|
||||||
sample:
|
sample:
|
||||||
issueTracker:
|
issueTracker:
|
||||||
provider: github
|
|
||||||
repo: owner/sample
|
|
||||||
path: ${tempDir.path}
|
|
||||||
issueAssistant:
|
|
||||||
eventSource:
|
eventSource:
|
||||||
type: gh/webhook-forward
|
type: gh/webhook-forward
|
||||||
reconciliation: startup-only
|
reconciliation: startup-only
|
||||||
reconcileInterval: 5m
|
reconcileInterval: 5m
|
||||||
|
repo: owner/sample
|
||||||
|
path: ${tempDir.path}
|
||||||
''');
|
''');
|
||||||
|
|
||||||
// When loading the app config from disk.
|
// When loading the app config from disk.
|
||||||
|
|
@ -783,7 +830,7 @@ projects:
|
||||||
(config.projects['sample']!.issueAssistant.eventSource
|
(config.projects['sample']!.issueAssistant.eventSource
|
||||||
as ChannelIssueEventSourceConfig)
|
as ChannelIssueEventSourceConfig)
|
||||||
.reconciliation,
|
.reconciliation,
|
||||||
IssueAssistantReconciliationKind.startupOnly,
|
IssueTrackerReconciliationKind.startupOnly,
|
||||||
);
|
);
|
||||||
|
|
||||||
// And the configured reconcile interval is parsed for channel recovery.
|
// And the configured reconcile interval is parsed for channel recovery.
|
||||||
|
|
@ -812,13 +859,11 @@ projects:
|
||||||
projects:
|
projects:
|
||||||
sample:
|
sample:
|
||||||
issueTracker:
|
issueTracker:
|
||||||
provider: github
|
|
||||||
repo: owner/sample
|
|
||||||
path: ${tempDir.path}
|
|
||||||
issueAssistant:
|
|
||||||
eventSource:
|
eventSource:
|
||||||
type: gh/webhook-forward
|
type: gh/webhook-forward
|
||||||
pollInterval: 45s
|
pollInterval: 45s
|
||||||
|
repo: owner/sample
|
||||||
|
path: ${tempDir.path}
|
||||||
''');
|
''');
|
||||||
|
|
||||||
// When loading the app config from disk.
|
// When loading the app config from disk.
|
||||||
|
|
@ -840,46 +885,51 @@ projects:
|
||||||
});
|
});
|
||||||
|
|
||||||
/// ```gherkin
|
/// ```gherkin
|
||||||
/// Scenario: Reject gh/webhook-forward for non-GitHub providers
|
/// Scenario: Reject a legacy Gitea provider that conflicts with gh/webhook-forward
|
||||||
/// Given a Gitea project config that opts into gh/webhook-forward
|
/// Given a project config whose legacy tracker provider is gitea
|
||||||
|
/// And the project opts into gh/webhook-forward
|
||||||
/// When loading the app config from disk
|
/// When loading the app config from disk
|
||||||
/// Then loading fails with a clear provider compatibility error
|
/// Then loading fails with a clear conflict error
|
||||||
/// ```
|
/// ```
|
||||||
test('Reject gh/webhook-forward for non-GitHub providers', () async {
|
test(
|
||||||
// Given a Gitea project config that opts into gh/webhook-forward.
|
'Reject a legacy Gitea provider that conflicts with gh/webhook-forward',
|
||||||
final tempDir = await Directory.systemTemp.createTemp(
|
() async {
|
||||||
'cws-webhook-forward-gitea-',
|
// Given a project config whose legacy tracker provider is gitea.
|
||||||
);
|
final tempDir = await Directory.systemTemp.createTemp(
|
||||||
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
|
'cws-webhook-forward-gitea-',
|
||||||
await configFile.writeAsString('''
|
);
|
||||||
|
final configFile = File(
|
||||||
|
p.join(tempDir.path, 'agent-orchestrator.yaml'),
|
||||||
|
);
|
||||||
|
await configFile.writeAsString('''
|
||||||
projects:
|
projects:
|
||||||
sample:
|
sample:
|
||||||
issueTracker:
|
issueTracker:
|
||||||
provider: gitea
|
provider: gitea
|
||||||
repo: owner/sample
|
|
||||||
path: ${tempDir.path}
|
|
||||||
issueAssistant:
|
|
||||||
eventSource:
|
eventSource:
|
||||||
type: gh/webhook-forward
|
type: gh/webhook-forward
|
||||||
|
repo: owner/sample
|
||||||
|
path: ${tempDir.path}
|
||||||
''');
|
''');
|
||||||
|
|
||||||
// When loading the app config from disk.
|
// When loading the app config from disk.
|
||||||
final load = AppConfig.load(configFile.path);
|
final load = AppConfig.load(configFile.path);
|
||||||
|
|
||||||
// Then loading fails with a clear provider compatibility error.
|
// Then loading fails with a clear conflict error.
|
||||||
await expectLater(
|
await expectLater(
|
||||||
load,
|
load,
|
||||||
throwsA(
|
throwsA(
|
||||||
isA<FormatException>().having(
|
isA<FormatException>().having(
|
||||||
(error) => error.message,
|
(error) => error.message,
|
||||||
'message',
|
'message',
|
||||||
contains(
|
contains(
|
||||||
'gh/webhook-forward requires issueTracker.provider: github',
|
'gh/webhook-forward implies issue tracker provider github, but the legacy configured provider is gitea',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
/// ```gherkin
|
/// ```gherkin
|
||||||
/// Scenario: Load tea/gosmee as the issue event source
|
/// Scenario: Load tea/gosmee as the issue event source
|
||||||
|
|
@ -896,12 +946,10 @@ projects:
|
||||||
projects:
|
projects:
|
||||||
sample:
|
sample:
|
||||||
issueTracker:
|
issueTracker:
|
||||||
provider: gitea
|
|
||||||
repo: owner/sample
|
|
||||||
path: ${tempDir.path}
|
|
||||||
issueAssistant:
|
|
||||||
eventSource:
|
eventSource:
|
||||||
type: tea/gosmee
|
type: tea/gosmee
|
||||||
|
repo: owner/sample
|
||||||
|
path: ${tempDir.path}
|
||||||
''');
|
''');
|
||||||
|
|
||||||
// When loading the app config from disk.
|
// When loading the app config from disk.
|
||||||
|
|
@ -910,7 +958,7 @@ projects:
|
||||||
// Then the project keeps tea/gosmee as its event source.
|
// Then the project keeps tea/gosmee as its event source.
|
||||||
expect(
|
expect(
|
||||||
config.projects['sample']!.issueAssistant.eventSource.type,
|
config.projects['sample']!.issueAssistant.eventSource.type,
|
||||||
IssueAssistantEventSourceKind.teaGosmee,
|
IssueTrackerEventSourceKind.teaGosmee,
|
||||||
);
|
);
|
||||||
|
|
||||||
// And the Gitea provider remains valid for that project.
|
// And the Gitea provider remains valid for that project.
|
||||||
|
|
@ -918,44 +966,51 @@ projects:
|
||||||
});
|
});
|
||||||
|
|
||||||
/// ```gherkin
|
/// ```gherkin
|
||||||
/// Scenario: Reject tea/gosmee for non-Gitea providers
|
/// Scenario: Reject a legacy GitHub provider that conflicts with tea/gosmee
|
||||||
/// Given a GitHub project config that opts into tea/gosmee
|
/// Given a project config whose legacy tracker provider is github
|
||||||
|
/// And the project opts into tea/gosmee
|
||||||
/// When loading the app config from disk
|
/// When loading the app config from disk
|
||||||
/// Then loading fails with a clear provider compatibility error
|
/// Then loading fails with a clear conflict error
|
||||||
/// ```
|
/// ```
|
||||||
test('Reject tea/gosmee for non-Gitea providers', () async {
|
test(
|
||||||
// Given a GitHub project config that opts into tea/gosmee.
|
'Reject a legacy GitHub provider that conflicts with tea/gosmee',
|
||||||
final tempDir = await Directory.systemTemp.createTemp(
|
() async {
|
||||||
'cws-gosmee-github-',
|
// Given a project config whose legacy tracker provider is github.
|
||||||
);
|
final tempDir = await Directory.systemTemp.createTemp(
|
||||||
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
|
'cws-gosmee-github-',
|
||||||
await configFile.writeAsString('''
|
);
|
||||||
|
final configFile = File(
|
||||||
|
p.join(tempDir.path, 'agent-orchestrator.yaml'),
|
||||||
|
);
|
||||||
|
await configFile.writeAsString('''
|
||||||
projects:
|
projects:
|
||||||
sample:
|
sample:
|
||||||
issueTracker:
|
issueTracker:
|
||||||
provider: github
|
provider: github
|
||||||
repo: owner/sample
|
|
||||||
path: ${tempDir.path}
|
|
||||||
issueAssistant:
|
|
||||||
eventSource:
|
eventSource:
|
||||||
type: tea/gosmee
|
type: tea/gosmee
|
||||||
|
repo: owner/sample
|
||||||
|
path: ${tempDir.path}
|
||||||
''');
|
''');
|
||||||
|
|
||||||
// When loading the app config from disk.
|
// When loading the app config from disk.
|
||||||
final load = AppConfig.load(configFile.path);
|
final load = AppConfig.load(configFile.path);
|
||||||
|
|
||||||
// Then loading fails with a clear provider compatibility error.
|
// Then loading fails with a clear conflict error.
|
||||||
await expectLater(
|
await expectLater(
|
||||||
load,
|
load,
|
||||||
throwsA(
|
throwsA(
|
||||||
isA<FormatException>().having(
|
isA<FormatException>().having(
|
||||||
(error) => error.message,
|
(error) => error.message,
|
||||||
'message',
|
'message',
|
||||||
contains('tea/gosmee requires issueTracker.provider: gitea'),
|
contains(
|
||||||
|
'tea/gosmee implies issue tracker provider gitea, but the legacy configured provider is github',
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
/// ```gherkin
|
/// ```gherkin
|
||||||
/// Scenario: Reject snake_case config keys
|
/// Scenario: Reject snake_case config keys
|
||||||
|
|
@ -1097,16 +1152,6 @@ projects:
|
||||||
.replaceFirst(' # timeout: 10m', ' timeout: 10m')
|
.replaceFirst(' # timeout: 10m', ' timeout: 10m')
|
||||||
.replaceFirst(' # issueAssistant:', ' issueAssistant:')
|
.replaceFirst(' # issueAssistant:', ' issueAssistant:')
|
||||||
.replaceFirst(' # enabled: true', ' enabled: true')
|
.replaceFirst(' # enabled: true', ' enabled: true')
|
||||||
.replaceFirst(' # eventSource:', ' eventSource:')
|
|
||||||
.replaceFirst(' # type: gh/gosmee', ' type: gh/gosmee')
|
|
||||||
.replaceFirst(
|
|
||||||
' # reconciliation: startup-only',
|
|
||||||
' reconciliation: startup-only',
|
|
||||||
)
|
|
||||||
.replaceFirst(
|
|
||||||
' # reconcileInterval: 30s',
|
|
||||||
' reconcileInterval: 30s',
|
|
||||||
)
|
|
||||||
.replaceFirst(
|
.replaceFirst(
|
||||||
' # mentionTriggers: ["@helper"]',
|
' # mentionTriggers: ["@helper"]',
|
||||||
' mentionTriggers: ["@helper"]',
|
' mentionTriggers: ["@helper"]',
|
||||||
|
|
@ -1138,10 +1183,8 @@ projects:
|
||||||
'@helper',
|
'@helper',
|
||||||
]);
|
]);
|
||||||
expect(
|
expect(
|
||||||
(config.projects['sample']!.issueAssistant.eventSource
|
config.projects['sample']!.issueAssistant.eventSource,
|
||||||
as ChannelIssueEventSourceConfig)
|
isA<PollingIssueEventSourceConfig>(),
|
||||||
.reconciliation,
|
|
||||||
IssueAssistantReconciliationKind.startupOnly,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,16 +25,16 @@ void main() {
|
||||||
final webhookForward = _RecordingIssueEventSource();
|
final webhookForward = _RecordingIssueEventSource();
|
||||||
final routed = RoutedIssueEventSource(
|
final routed = RoutedIssueEventSource(
|
||||||
sources: {
|
sources: {
|
||||||
IssueAssistantEventSourceKind.ghPolling: polling,
|
IssueTrackerEventSourceKind.ghPolling: polling,
|
||||||
IssueAssistantEventSourceKind.teaPolling: polling,
|
IssueTrackerEventSourceKind.teaPolling: polling,
|
||||||
IssueAssistantEventSourceKind.ghWebhookForward: webhookForward,
|
IssueTrackerEventSourceKind.ghWebhookForward: webhookForward,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// And a project that uses gh/webhook-forward with startup-only reconciliation.
|
// And a project that uses gh/webhook-forward with startup-only reconciliation.
|
||||||
final project = _project(
|
final project = _project(
|
||||||
eventSource: IssueAssistantEventSourceKind.ghWebhookForward,
|
eventSource: IssueTrackerEventSourceKind.ghWebhookForward,
|
||||||
reconciliation: IssueAssistantReconciliationKind.startupOnly,
|
reconciliation: IssueTrackerReconciliationKind.startupOnly,
|
||||||
);
|
);
|
||||||
|
|
||||||
// When the routed event source starts in long-running mode.
|
// When the routed event source starts in long-running mode.
|
||||||
|
|
@ -64,16 +64,16 @@ void main() {
|
||||||
final webhookForward = _RecordingIssueEventSource();
|
final webhookForward = _RecordingIssueEventSource();
|
||||||
final routed = RoutedIssueEventSource(
|
final routed = RoutedIssueEventSource(
|
||||||
sources: {
|
sources: {
|
||||||
IssueAssistantEventSourceKind.ghPolling: polling,
|
IssueTrackerEventSourceKind.ghPolling: polling,
|
||||||
IssueAssistantEventSourceKind.teaPolling: polling,
|
IssueTrackerEventSourceKind.teaPolling: polling,
|
||||||
IssueAssistantEventSourceKind.ghWebhookForward: webhookForward,
|
IssueTrackerEventSourceKind.ghWebhookForward: webhookForward,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// And a project that uses gh/webhook-forward with continuous reconciliation.
|
// And a project that uses gh/webhook-forward with continuous reconciliation.
|
||||||
final project = _project(
|
final project = _project(
|
||||||
eventSource: IssueAssistantEventSourceKind.ghWebhookForward,
|
eventSource: IssueTrackerEventSourceKind.ghWebhookForward,
|
||||||
reconciliation: IssueAssistantReconciliationKind.continuous,
|
reconciliation: IssueTrackerReconciliationKind.continuous,
|
||||||
);
|
);
|
||||||
|
|
||||||
// When the routed event source starts in long-running mode.
|
// When the routed event source starts in long-running mode.
|
||||||
|
|
@ -115,8 +115,8 @@ class _RecordingIssueEventSource implements IssueEventSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
ProjectConfig _project({
|
ProjectConfig _project({
|
||||||
required IssueAssistantEventSourceKind eventSource,
|
required IssueTrackerEventSourceKind eventSource,
|
||||||
required IssueAssistantReconciliationKind reconciliation,
|
required IssueTrackerReconciliationKind reconciliation,
|
||||||
}) {
|
}) {
|
||||||
return ProjectConfig(
|
return ProjectConfig(
|
||||||
key: 'sample',
|
key: 'sample',
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ void main() {
|
||||||
issueAssistant: IssueAssistantConfig(
|
issueAssistant: IssueAssistantConfig(
|
||||||
enabled: true,
|
enabled: true,
|
||||||
eventSource: const PollingIssueEventSourceConfig(
|
eventSource: const PollingIssueEventSourceConfig(
|
||||||
type: IssueAssistantEventSourceKind.ghPolling,
|
type: IssueTrackerEventSourceKind.ghPolling,
|
||||||
pollInterval: Duration(seconds: 30),
|
pollInterval: Duration(seconds: 30),
|
||||||
),
|
),
|
||||||
maxConcurrentIssues: 3,
|
maxConcurrentIssues: 3,
|
||||||
|
|
@ -131,7 +131,7 @@ void main() {
|
||||||
issueAssistant: IssueAssistantConfig(
|
issueAssistant: IssueAssistantConfig(
|
||||||
enabled: true,
|
enabled: true,
|
||||||
eventSource: const PollingIssueEventSourceConfig(
|
eventSource: const PollingIssueEventSourceConfig(
|
||||||
type: IssueAssistantEventSourceKind.glabPolling,
|
type: IssueTrackerEventSourceKind.glabPolling,
|
||||||
pollInterval: Duration(seconds: 30),
|
pollInterval: Duration(seconds: 30),
|
||||||
),
|
),
|
||||||
maxConcurrentIssues: 3,
|
maxConcurrentIssues: 3,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue