codee-agent 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codee_agent-0.1.0 → codee_agent-0.2.0}/PKG-INFO +1 -1
- {codee_agent-0.1.0 → codee_agent-0.2.0}/pyproject.toml +1 -1
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/admin.py +7 -12
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/admin_service.py +2 -2
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/lib/test_trigger_cron_skills.py +32 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/lib/trigger_aws_sqs_skills.py +27 -1
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/test_admin_service.py +0 -1
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/test_executor.py +2 -2
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_main_context/context.py +2 -1
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_tasks_azure_devops/oauth.py +8 -5
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_tasks_azure_devops/provider.py +13 -3
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_tasks_azure_devops/test.py +42 -18
- {codee_agent-0.1.0 → codee_agent-0.2.0}/LICENSE +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/README.md +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/.gitignore +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/__init__.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/admin_api.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/admin_cli.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/executor.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/init_cli.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/lib/__init__.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/lib/cron_describe.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/lib/runs_db.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/lib/test_runs_db.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/lib/test_trigger_issue_skills.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/lib/trigger_cron_skills.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/lib/trigger_email_skills.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/lib/trigger_issue_skills.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/mail_server.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/start_cli.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/templates/AGENTS.md +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/templates/CLAUDE.md +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/templates/skills/story-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/templates/skills/story-developer/SKILL.md +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/templates/skills/story-planner/SKILL.md +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/templates/skills/story-planner/assets/readme-template.md +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/templates/skills/story-qa/SKILL.md +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/templates/skills/task-developer/SKILL.md +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/templates/skills/task-qa/SKILL.md +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/test_admin_api.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/test_admin_cli.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/test_init_cli.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/test_memory_index.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/test_start_cli.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/workflow_graph.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_admin/__init__.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_admin/codee_admin.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_agent_abstract/__init__.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_agent_abstract/provider.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_agent_claude_code/__init__.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_agent_claude_code/provider.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_agent_github_copilot/__init__.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_agent_github_copilot/provider.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_agent_github_copilot/test.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_database/__init__.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_database/database.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_database/oauth_tokens.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_main_context/__init__.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_main_context/logging.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_main_context/test_logging.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_tasks_abstract/__init__.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_tasks_abstract/provider.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_tasks_azure_devops/__init__.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_tasks_jira/__init__.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_tasks_jira/provider.py +0 -0
- {codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee_tasks_jira/test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codee-agent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A virtual co-worker that picks up Jira and Azure DevOps tasks and solves them with coding agents.
|
|
5
5
|
Keywords: ai,agent,jira,azure-devops,claude-code,github-copilot,automation
|
|
6
6
|
Author: Denis Kibalko
|
|
@@ -126,7 +126,6 @@ class AdminState(rx.State):
|
|
|
126
126
|
jira_api_token: str = ""
|
|
127
127
|
jira_project: str = ""
|
|
128
128
|
azure_organization_url: str = ""
|
|
129
|
-
azure_project: str = ""
|
|
130
129
|
azure_tenant_id: str = ""
|
|
131
130
|
azure_client_id: str = ""
|
|
132
131
|
azure_client_secret: str = ""
|
|
@@ -484,7 +483,6 @@ class AdminState(rx.State):
|
|
|
484
483
|
self.jira_api_token = jira.get("api_token", "")
|
|
485
484
|
self.jira_project = jira.get("project", "")
|
|
486
485
|
self.azure_organization_url = azure.get("organization_url", "")
|
|
487
|
-
self.azure_project = azure.get("project", "")
|
|
488
486
|
self.azure_tenant_id = azure.get("tenant_id", "")
|
|
489
487
|
self.azure_client_id = azure.get("client_id", "")
|
|
490
488
|
self.azure_client_secret = azure.get("client_secret", "")
|
|
@@ -533,9 +531,6 @@ class AdminState(rx.State):
|
|
|
533
531
|
def set_azure_organization_url(self, value: str) -> None:
|
|
534
532
|
self.azure_organization_url = value
|
|
535
533
|
|
|
536
|
-
def set_azure_project(self, value: str) -> None:
|
|
537
|
-
self.azure_project = value
|
|
538
|
-
|
|
539
534
|
def set_azure_tenant_id(self, value: str) -> None:
|
|
540
535
|
self.azure_tenant_id = value
|
|
541
536
|
|
|
@@ -549,7 +544,7 @@ class AdminState(rx.State):
|
|
|
549
544
|
def azure_can_connect(self) -> bool:
|
|
550
545
|
"""Everything the authorization request and the later queries need."""
|
|
551
546
|
return all(value.strip() for value in (
|
|
552
|
-
self.azure_organization_url,
|
|
547
|
+
self.azure_organization_url,
|
|
553
548
|
self.azure_client_id, self.azure_client_secret))
|
|
554
549
|
|
|
555
550
|
def connect_azure_devops(self) -> Any:
|
|
@@ -560,7 +555,7 @@ class AdminState(rx.State):
|
|
|
560
555
|
"""
|
|
561
556
|
if not self.azure_can_connect:
|
|
562
557
|
return rx.toast.error(
|
|
563
|
-
"Fill in organization URL,
|
|
558
|
+
"Fill in organization URL, client ID and client secret first.")
|
|
564
559
|
error = self._persist_settings()
|
|
565
560
|
if error:
|
|
566
561
|
return rx.toast.error(error)
|
|
@@ -591,7 +586,6 @@ class AdminState(rx.State):
|
|
|
591
586
|
if self.tasks_provider == "jira"
|
|
592
587
|
else {
|
|
593
588
|
"organization_url": self.azure_organization_url,
|
|
594
|
-
"project": self.azure_project,
|
|
595
589
|
"tenant_id": self.azure_tenant_id,
|
|
596
590
|
"client_id": self.azure_client_id,
|
|
597
591
|
"client_secret": self.azure_client_secret,
|
|
@@ -1443,7 +1437,7 @@ def azure_instructions() -> rx.Component:
|
|
|
1443
1437
|
"→ user_impersonation → Add. Entra publishes no read-only scope "
|
|
1444
1438
|
"for Azure DevOps; Codee only ever issues read calls, and you can "
|
|
1445
1439
|
"narrow it further by connecting with an account that has Readers "
|
|
1446
|
-
"access to the
|
|
1440
|
+
"access to only the projects it should see."),
|
|
1447
1441
|
azure_step(4, "Create a client secret",
|
|
1448
1442
|
"Certificates & secrets → New client secret. Copy the Value "
|
|
1449
1443
|
"column immediately — Azure hides it once you leave the page."),
|
|
@@ -1485,9 +1479,10 @@ def azure_fields() -> rx.Component:
|
|
|
1485
1479
|
field("Organization URL", rx.input(value=AdminState.azure_organization_url,
|
|
1486
1480
|
on_change=AdminState.set_azure_organization_url,
|
|
1487
1481
|
placeholder="https://dev.azure.com/your-org",
|
|
1488
|
-
width="100%")
|
|
1489
|
-
|
|
1490
|
-
|
|
1482
|
+
width="100%"),
|
|
1483
|
+
hint=rx.text("Work items are picked up from every project in this "
|
|
1484
|
+
"organization the connected account can read.",
|
|
1485
|
+
color=MUTED, font_size="0.8rem")),
|
|
1491
1486
|
field("Application (client) ID", rx.input(value=AdminState.azure_client_id,
|
|
1492
1487
|
on_change=AdminState.set_azure_client_id,
|
|
1493
1488
|
width="100%")),
|
|
@@ -949,8 +949,8 @@ class AdminService:
|
|
|
949
949
|
"""
|
|
950
950
|
config = azure_oauth.OAuthConfig.from_settings(self.load_settings())
|
|
951
951
|
if not config.is_complete():
|
|
952
|
-
return False, ("Fill in organization URL,
|
|
953
|
-
"
|
|
952
|
+
return False, ("Fill in organization URL, client ID and client "
|
|
953
|
+
"secret before connecting.")
|
|
954
954
|
state = azure_oauth.new_state()
|
|
955
955
|
code_verifier = azure_oauth.new_code_verifier()
|
|
956
956
|
redirect_uri = self.azure_redirect_uri()
|
|
@@ -6,11 +6,13 @@ import unittest
|
|
|
6
6
|
import sys
|
|
7
7
|
from datetime import datetime, timedelta
|
|
8
8
|
from pathlib import Path
|
|
9
|
+
from unittest.mock import patch
|
|
9
10
|
|
|
10
11
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
11
12
|
|
|
12
13
|
from codee_main_context.context import CodeeMainContext
|
|
13
14
|
|
|
15
|
+
from codee.lib import trigger_aws_sqs_skills as trigger_aws_sqs_skills_module
|
|
14
16
|
from codee.lib.trigger_aws_sqs_skills import (
|
|
15
17
|
AwsSqsMessage,
|
|
16
18
|
find_aws_sqs_triggered_skills,
|
|
@@ -480,6 +482,36 @@ class CronSkillTests(unittest.TestCase):
|
|
|
480
482
|
self.assertTrue(calls[0][1])
|
|
481
483
|
self.assertEqual(sqs_source.deleted, [message])
|
|
482
484
|
|
|
485
|
+
def test_reconcile_survives_unconfigured_aws_client(self):
|
|
486
|
+
def run_claude(user_message: str, session_id: str, model: str = "") -> str:
|
|
487
|
+
raise AssertionError("should not run without an SQS client")
|
|
488
|
+
|
|
489
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
490
|
+
root = Path(temp_dir)
|
|
491
|
+
skills_dir = root / "skills"
|
|
492
|
+
skill_dir = skills_dir / "sqs-check"
|
|
493
|
+
skill_dir.mkdir(parents=True)
|
|
494
|
+
(skill_dir / "SKILL.md").write_text(
|
|
495
|
+
"---\n"
|
|
496
|
+
"name: SQS Check\n"
|
|
497
|
+
"disable-model-invocation: true\n"
|
|
498
|
+
"x-codee-trigger: aws-sqs\n"
|
|
499
|
+
"x-codee-aws-sqs-queue: codee-queue\n"
|
|
500
|
+
"---\n\n"
|
|
501
|
+
"Process this content: {CONTENT}\n"
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
with patch.object(
|
|
505
|
+
trigger_aws_sqs_skills_module,
|
|
506
|
+
"Boto3AwsSqsMessageSource",
|
|
507
|
+
side_effect=Exception("You must specify a region."),
|
|
508
|
+
):
|
|
509
|
+
trigger_aws_sqs_skills(
|
|
510
|
+
run_claude,
|
|
511
|
+
skills_dir=skills_dir,
|
|
512
|
+
main_context=_ctx(root),
|
|
513
|
+
)
|
|
514
|
+
|
|
483
515
|
|
|
484
516
|
if __name__ == "__main__":
|
|
485
517
|
unittest.main()
|
|
@@ -53,7 +53,9 @@ def trigger_aws_sqs_skills(
|
|
|
53
53
|
return
|
|
54
54
|
|
|
55
55
|
if sqs_message_source is None:
|
|
56
|
-
sqs_message_source =
|
|
56
|
+
sqs_message_source = _build_boto3_message_source()
|
|
57
|
+
if sqs_message_source is None:
|
|
58
|
+
return
|
|
57
59
|
|
|
58
60
|
for skill in skills:
|
|
59
61
|
try:
|
|
@@ -131,6 +133,30 @@ def find_aws_sqs_triggered_skills(skills_dir: Path = SKILLS_DIR) -> list[AwsSqsT
|
|
|
131
133
|
return skills
|
|
132
134
|
|
|
133
135
|
|
|
136
|
+
def _build_boto3_message_source() -> "Boto3AwsSqsMessageSource | None":
|
|
137
|
+
"""Build the SQS client, or return None when AWS isn't configured.
|
|
138
|
+
|
|
139
|
+
boto3 raises (NoRegionError, missing credentials, boto3 not installed) at
|
|
140
|
+
client construction. That must not abort the executor tick -- the rest of
|
|
141
|
+
run_once() still has cron skills, email skills and tasks to process -- so we
|
|
142
|
+
report it once per process and let SQS triggers stay idle.
|
|
143
|
+
"""
|
|
144
|
+
global _boto3_unavailable_reported
|
|
145
|
+
try:
|
|
146
|
+
return Boto3AwsSqsMessageSource()
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
if not _boto3_unavailable_reported:
|
|
149
|
+
_boto3_unavailable_reported = True
|
|
150
|
+
print(
|
|
151
|
+
f"[aws_sqs_skills] AWS SQS client unavailable ({exc}); SQS triggers "
|
|
152
|
+
"stay idle until AWS is configured (e.g. AWS_DEFAULT_REGION)."
|
|
153
|
+
)
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
_boto3_unavailable_reported = False
|
|
158
|
+
|
|
159
|
+
|
|
134
160
|
class Boto3AwsSqsMessageSource:
|
|
135
161
|
def __init__(self, *, visibility_timeout: int = 7200) -> None:
|
|
136
162
|
import boto3
|
|
@@ -55,7 +55,6 @@ class RefreshConfigTest(unittest.TestCase):
|
|
|
55
55
|
tasks_provider=TasksProvider.AZURE_DEVOPS,
|
|
56
56
|
credentials={TasksProvider.AZURE_DEVOPS.value: {
|
|
57
57
|
"organization_url": "https://dev.azure.com/acme",
|
|
58
|
-
"project": "Core",
|
|
59
58
|
"tenant_id": "tenant-1",
|
|
60
59
|
"client_id": "client-1",
|
|
61
60
|
"client_secret": "secret-1",
|
|
@@ -104,7 +103,8 @@ class RefreshConfigTest(unittest.TestCase):
|
|
|
104
103
|
|
|
105
104
|
self._save(
|
|
106
105
|
tasks_provider=TasksProvider.AZURE_DEVOPS,
|
|
107
|
-
credentials={TasksProvider.AZURE_DEVOPS.value: {
|
|
106
|
+
credentials={TasksProvider.AZURE_DEVOPS.value: {
|
|
107
|
+
"organization_url": "https://dev.azure.com/acme"}},
|
|
108
108
|
)
|
|
109
109
|
with patch.dict(executor._TASKS_PROVIDERS,
|
|
110
110
|
{TasksProvider.AZURE_DEVOPS: _Exploding}):
|
|
@@ -59,9 +59,10 @@ TASKS_PROVIDER_FIELDS: dict[TasksProvider, list[CredentialField]] = {
|
|
|
59
59
|
# Azure DevOps authenticates through an Entra ID app registration, so the
|
|
60
60
|
# stored credentials describe the app; the tokens it yields live in SQLite
|
|
61
61
|
# (codee_database.oauth_tokens) rather than in settings.json.
|
|
62
|
+
# There is no project field: work items are queried across the whole
|
|
63
|
+
# organization, which is the scope the granted access has anyway.
|
|
62
64
|
TasksProvider.AZURE_DEVOPS: [
|
|
63
65
|
CredentialField("organization_url", "Organization URL"),
|
|
64
|
-
CredentialField("project", "Project"),
|
|
65
66
|
CredentialField("tenant_id", "Directory (tenant) ID"),
|
|
66
67
|
CredentialField("client_id", "Application (client) ID"),
|
|
67
68
|
CredentialField("client_secret", "Client secret", secret=True),
|
|
@@ -64,10 +64,14 @@ class AzureDevOpsAuthError(RuntimeError):
|
|
|
64
64
|
|
|
65
65
|
@dataclass(frozen=True)
|
|
66
66
|
class OAuthConfig:
|
|
67
|
-
"""The Entra app registration, as captured in settings.json.
|
|
67
|
+
"""The Entra app registration, as captured in settings.json.
|
|
68
|
+
|
|
69
|
+
There is no project here: queries run across the whole organization. A
|
|
70
|
+
project would only ever have been a filter — the access Entra grants covers
|
|
71
|
+
the organization however it is set.
|
|
72
|
+
"""
|
|
68
73
|
|
|
69
74
|
organization_url: str = ""
|
|
70
|
-
project: str = ""
|
|
71
75
|
tenant_id: str = ""
|
|
72
76
|
client_id: str = ""
|
|
73
77
|
client_secret: str = ""
|
|
@@ -77,7 +81,6 @@ class OAuthConfig:
|
|
|
77
81
|
creds = settings.credentials.get(PROVIDER, {})
|
|
78
82
|
return cls(
|
|
79
83
|
organization_url=(creds.get("organization_url") or "").strip().rstrip("/"),
|
|
80
|
-
project=(creds.get("project") or "").strip(),
|
|
81
84
|
tenant_id=(creds.get("tenant_id") or "").strip(),
|
|
82
85
|
client_id=(creds.get("client_id") or "").strip(),
|
|
83
86
|
client_secret=(creds.get("client_secret") or "").strip(),
|
|
@@ -85,8 +88,8 @@ class OAuthConfig:
|
|
|
85
88
|
|
|
86
89
|
def is_complete(self) -> bool:
|
|
87
90
|
"""Whether we have everything needed to run the flow and query tasks."""
|
|
88
|
-
return bool(self.organization_url and self.
|
|
89
|
-
and self.
|
|
91
|
+
return bool(self.organization_url and self.client_id
|
|
92
|
+
and self.client_secret)
|
|
90
93
|
|
|
91
94
|
@property
|
|
92
95
|
def tenant(self) -> str:
|
|
@@ -91,7 +91,7 @@ class AzureDevOpsTasksProvider(AbstractTasksProvider):
|
|
|
91
91
|
connection = self._auth.connection() or {}
|
|
92
92
|
account = connection.get("account") or "connected account"
|
|
93
93
|
return (f"Azure DevOps {self._config.organization_url} "
|
|
94
|
-
f"(
|
|
94
|
+
f"(all projects, assignee {account})")
|
|
95
95
|
|
|
96
96
|
def get_tasks(self, statuses: list[str]) -> list[Task]:
|
|
97
97
|
"""Fetch work items assigned to the connected account in the given states."""
|
|
@@ -120,8 +120,11 @@ class AzureDevOpsTasksProvider(AbstractTasksProvider):
|
|
|
120
120
|
for item_id in ids if item_id in by_id]
|
|
121
121
|
|
|
122
122
|
def _query_work_item_ids(self, token: str, statuses: list[str]) -> list[int]:
|
|
123
|
+
# Organization-scoped, like the batch fetch below: the endpoint's
|
|
124
|
+
# project segment is optional, and leaving it off is what lets one query
|
|
125
|
+
# span every project the connected account can read.
|
|
123
126
|
response = requests.post(
|
|
124
|
-
f"{self._config.organization_url}/
|
|
127
|
+
f"{self._config.organization_url}/_apis/wit/wiql",
|
|
125
128
|
params={"api-version": API_VERSION, "$top": _MAX_TASKS},
|
|
126
129
|
json={"query": self._build_wiql(statuses)},
|
|
127
130
|
headers=self._headers(token),
|
|
@@ -132,7 +135,14 @@ class AzureDevOpsTasksProvider(AbstractTasksProvider):
|
|
|
132
135
|
return [item["id"] for item in work_items][:_BATCH_LIMIT]
|
|
133
136
|
|
|
134
137
|
def _build_wiql(self, statuses: list[str]) -> str:
|
|
135
|
-
"""WIQL for Codee work items owned by the connected account, highest priority first.
|
|
138
|
+
"""WIQL for Codee work items owned by the connected account, highest priority first.
|
|
139
|
+
|
|
140
|
+
Nothing here names a project, and no ``@project`` macro is used — that
|
|
141
|
+
is what keeps the query valid with no project in the route, so it spans
|
|
142
|
+
the organization. What comes back is still narrow: only the Codee work
|
|
143
|
+
item types, only the states asked for, and only what is assigned to the
|
|
144
|
+
connected account.
|
|
145
|
+
"""
|
|
136
146
|
quoted_statuses = ", ".join(_quote_wiql(status) for status in statuses)
|
|
137
147
|
quoted_types = ", ".join(_quote_wiql(item_type)
|
|
138
148
|
for item_type in _WORK_ITEM_TYPES)
|
|
@@ -18,7 +18,6 @@ from codee_tasks_azure_devops.provider import AzureDevOpsTasksProvider
|
|
|
18
18
|
def _config(**overrides) -> OAuthConfig:
|
|
19
19
|
values = {
|
|
20
20
|
"organization_url": "https://dev.azure.com/acme",
|
|
21
|
-
"project": "Core",
|
|
22
21
|
"tenant_id": "tenant-1",
|
|
23
22
|
"client_id": "client-1",
|
|
24
23
|
"client_secret": "secret-1",
|
|
@@ -39,16 +38,16 @@ def _iso(offset: timedelta) -> str:
|
|
|
39
38
|
return (datetime.now(timezone.utc) + offset).isoformat()
|
|
40
39
|
|
|
41
40
|
|
|
41
|
+
def _settings(**credentials) -> Settings:
|
|
42
|
+
return Settings(tasks_provider=TasksProvider.AZURE_DEVOPS,
|
|
43
|
+
credentials={"azure_devops": credentials})
|
|
44
|
+
|
|
45
|
+
|
|
42
46
|
class OAuthConfigTest(unittest.TestCase):
|
|
43
47
|
def test_from_settings_reads_provider_credentials(self) -> None:
|
|
44
|
-
settings =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"organization_url": "https://dev.azure.com/acme/",
|
|
48
|
-
"project": "Core",
|
|
49
|
-
"client_id": "client-1",
|
|
50
|
-
"client_secret": "secret-1",
|
|
51
|
-
}})
|
|
48
|
+
settings = _settings(organization_url="https://dev.azure.com/acme/",
|
|
49
|
+
client_id="client-1",
|
|
50
|
+
client_secret="secret-1")
|
|
52
51
|
|
|
53
52
|
config = OAuthConfig.from_settings(settings)
|
|
54
53
|
|
|
@@ -56,6 +55,16 @@ class OAuthConfigTest(unittest.TestCase):
|
|
|
56
55
|
self.assertEqual(config.organization_url, "https://dev.azure.com/acme")
|
|
57
56
|
self.assertTrue(config.is_complete())
|
|
58
57
|
|
|
58
|
+
def test_a_leftover_project_credential_is_ignored(self) -> None:
|
|
59
|
+
# Settings written when a project field still existed must not keep the
|
|
60
|
+
# queries narrowed to it; there is nothing left that reads the key.
|
|
61
|
+
config = OAuthConfig.from_settings(
|
|
62
|
+
_settings(organization_url="https://dev.azure.com/acme",
|
|
63
|
+
project="Core", client_id="c", client_secret="s"))
|
|
64
|
+
|
|
65
|
+
self.assertTrue(config.is_complete())
|
|
66
|
+
self.assertNotIn("Core", str(config))
|
|
67
|
+
|
|
59
68
|
def test_is_incomplete_without_a_secret(self) -> None:
|
|
60
69
|
self.assertFalse(_config(client_secret="").is_complete())
|
|
61
70
|
|
|
@@ -299,15 +308,11 @@ class TasksProviderTest(unittest.TestCase):
|
|
|
299
308
|
self._temporary = tempfile.TemporaryDirectory()
|
|
300
309
|
self.addCleanup(self._temporary.cleanup)
|
|
301
310
|
self.context = CodeeMainContext(data_dir=Path(self._temporary.name))
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
"client_id": "client-1",
|
|
308
|
-
"client_secret": "secret-1",
|
|
309
|
-
}})
|
|
310
|
-
self.provider = AzureDevOpsTasksProvider(settings, self.context)
|
|
311
|
+
self.provider = AzureDevOpsTasksProvider(
|
|
312
|
+
_settings(organization_url="https://dev.azure.com/acme",
|
|
313
|
+
client_id="client-1",
|
|
314
|
+
client_secret="secret-1"),
|
|
315
|
+
self.context)
|
|
311
316
|
|
|
312
317
|
def _connect(self) -> None:
|
|
313
318
|
oauth_tokens.save_tokens(
|
|
@@ -328,6 +333,25 @@ class TasksProviderTest(unittest.TestCase):
|
|
|
328
333
|
self.assertIn(
|
|
329
334
|
"[System.WorkItemType] IN ('Codee Task', 'Codee Story')", wiql)
|
|
330
335
|
|
|
336
|
+
def test_wiql_names_no_project_at_all(self) -> None:
|
|
337
|
+
wiql = self.provider._build_wiql(["Ready"])
|
|
338
|
+
|
|
339
|
+
self.assertNotIn("System.TeamProject", wiql)
|
|
340
|
+
# @project would need a project in the route, which there isn't.
|
|
341
|
+
self.assertNotIn("@project", wiql)
|
|
342
|
+
|
|
343
|
+
def test_the_query_is_organization_scoped(self) -> None:
|
|
344
|
+
self._connect()
|
|
345
|
+
with patch("codee_tasks_azure_devops.provider.requests.post",
|
|
346
|
+
side_effect=[_response({"workItems": []})]) as post:
|
|
347
|
+
self.provider.get_tasks(["Ready"])
|
|
348
|
+
|
|
349
|
+
self.assertEqual(post.call_args.args[0],
|
|
350
|
+
"https://dev.azure.com/acme/_apis/wit/wiql")
|
|
351
|
+
|
|
352
|
+
def test_describe_says_the_query_covers_all_projects(self) -> None:
|
|
353
|
+
self.assertIn("all projects", self.provider.describe())
|
|
354
|
+
|
|
331
355
|
def test_not_configured_until_oauth_completes(self) -> None:
|
|
332
356
|
self.assertFalse(self.provider.is_configured())
|
|
333
357
|
self._connect()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.1.0 → codee_agent-0.2.0}/src/codee/templates/skills/story-code-reviewer/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|