-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
feat: add Asana ticket detection to ticket compliance check #2430
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 9 commits
bafe543
5a70b98
99b9f74
3586598
2b76ed6
3ce5450
0573be2
4caee4c
65460ce
bc043c7
a096303
800a5c2
cf17141
1c1a693
61df3f9
c928e6c
557b28c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,29 @@ def find_jira_tickets(text): | |
| return list(tickets) | ||
|
|
||
|
|
||
| _ASANA_TASK_URL_PATTERN = re.compile( | ||
| r'https://app\.asana\.com/0/(\d+)/(\d+)' | ||
| ) | ||
|
|
||
|
|
||
| def find_asana_tickets(text: str) -> list: | ||
| """Extract Asana task references from text. | ||
|
|
||
| Supports full Asana URLs (``https://app.asana.com/0/{project_id}/{task_id}``). | ||
| Returns a list of unique task URLs. | ||
|
|
||
| Args: | ||
| text: The text to scan for Asana task references. | ||
|
|
||
| Returns: | ||
| A list of Asana task URLs. | ||
| """ | ||
| tickets = set() | ||
| for match in _ASANA_TASK_URL_PATTERN.finditer(text): | ||
| tickets.add(match.group(0)) | ||
| return sorted(tickets) | ||
|
|
||
|
|
||
| def extract_ticket_links_from_pr_description(pr_description, repo_path, base_url_html='https://github.com'): | ||
| """ | ||
| Extract all ticket links from PR description | ||
|
|
@@ -123,16 +146,42 @@ async def extract_tickets(git_provider): | |
| if link not in seen: | ||
| seen.add(link) | ||
| merged.append(link) | ||
|
|
||
| # Also detect Asana ticket references in the PR description | ||
| asana_tickets = find_asana_tickets(user_description) | ||
| for link in asana_tickets: | ||
| if link not in seen: | ||
| seen.add(link) | ||
| merged.append(link) | ||
| asana_links = [t for t in merged if t.startswith("https://app.asana.com/")] | ||
| github_like = [t for t in merged if not t.startswith("https://app.asana.com/")] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. Asana detection github-only find_asana_tickets() is only invoked inside the isinstance(git_provider, GithubProvider) branch of extract_tickets(), so non-GitHub providers will never include Asana references in related_tickets. This makes Asana ticket compliance visibility silently unavailable for other configured providers. Agent Prompt
|
||
| if len(merged) > 3: | ||
| get_logger().info(f"Too many tickets (description + branch): {len(merged)}") | ||
| tickets = merged[:3] | ||
| # Reserve at least one slot for an Asana reference when | ||
| # present so it is not systematically dropped. | ||
| asana_slot = asana_links[:1] | ||
| gh_slots = 3 - len(asana_slot) | ||
| tickets = github_like[:gh_slots] + asana_slot | ||
| else: | ||
| tickets = merged | ||
| tickets_content = [] | ||
|
|
||
| if tickets: | ||
|
|
||
| for ticket in tickets: | ||
| # Skip Asana URLs — these are external references, | ||
| # included for visibility but cannot be fetched via GitHub API. | ||
| if ticket.startswith("https://app.asana.com/"): | ||
| tickets_content.append({ | ||
| "ticket_id": ticket, | ||
| "ticket_url": ticket, | ||
| "title": f"Asana Task: {ticket}", | ||
| "body": ("Asana task referenced in PR description. " | ||
| "Fetch task details from Asana for full context."), | ||
| "labels": "", | ||
| }) | ||
| continue | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. extract_tickets skips asana fetching The new Asana handling explicitly avoids fetching any Asana task data and only adds a placeholder entry, so Asana cannot be used as a real ticket provider end-to-end. This violates the requirement to integrate an Asana Ticket Provider into the existing ticket fetching/compliance infrastructure. Agent Prompt
|
||
|
|
||
| repo_name, original_issue_number = git_provider._parse_issue_url(ticket) | ||
|
|
||
| try: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| """ | ||
| Unit tests for Asana ticket detection in ticket_pr_compliance_check.py. | ||
|
|
||
| Tests cover: | ||
| - Full Asana URL detection | ||
| - Edge cases (mixed content, no tickets, duplicates) | ||
| """ | ||
| from pr_agent.git_providers.github_provider import GithubProvider | ||
| from pr_agent.tools.ticket_pr_compliance_check import extract_tickets, find_asana_tickets | ||
|
|
||
|
|
||
| class _Issue: | ||
| def __init__(self, number): | ||
| self.number = number | ||
| self.title = f"Issue {number}" | ||
| self.body = f"Issue {number} body" | ||
| self.labels = [] | ||
|
|
||
|
|
||
| class _Repo: | ||
| def get_issue(self, number): | ||
| return _Issue(number) | ||
|
|
||
|
|
||
| class _GithubProvider(GithubProvider): | ||
| repo = "owner/repo" | ||
| base_url_html = "https://github.com" | ||
| repo_obj = _Repo() | ||
|
|
||
| def __init__(self, description): | ||
| self.description = description | ||
|
|
||
| def get_user_description(self): | ||
| return self.description | ||
|
|
||
| def get_pr_branch(self): | ||
| return "" | ||
|
|
||
| def _parse_issue_url(self, ticket): | ||
| return self.repo, int(ticket.rsplit("/", 1)[-1]) | ||
|
|
||
| def fetch_sub_issues(self, ticket): | ||
| return [] | ||
|
|
||
|
|
||
| class TestFindAsanaTickets: | ||
| """Tests for find_asana_tickets().""" | ||
|
|
||
| def test_detects_full_asana_url(self): | ||
| """Full Asana task URLs should be detected.""" | ||
| text = "See https://app.asana.com/0/123456/789012 for details" | ||
| tickets = find_asana_tickets(text) | ||
| assert "https://app.asana.com/0/123456/789012" in tickets | ||
|
|
||
| def test_detects_multiple_urls(self): | ||
| """Multiple Asana URLs should all be found.""" | ||
| text = ( | ||
| "See https://app.asana.com/0/11/111111111111" | ||
| " and https://app.asana.com/0/22/333333333333" | ||
| ) | ||
| tickets = find_asana_tickets(text) | ||
| assert len(tickets) == 2 | ||
|
|
||
| def test_deduplicates_identical_urls(self): | ||
| """Duplicate references to the same URL should be deduplicated.""" | ||
| text = ( | ||
| "https://app.asana.com/0/1/123456789012" | ||
| " mentioned twice: https://app.asana.com/0/1/123456789012" | ||
| ) | ||
| tickets = find_asana_tickets(text) | ||
| assert len(tickets) == 1 | ||
|
|
||
| def test_returns_empty_for_no_tickets(self): | ||
| """Text without Asana references returns an empty list.""" | ||
| text = "No tickets here, just regular text" | ||
| tickets = find_asana_tickets(text) | ||
| assert tickets == [] | ||
|
|
||
| def test_returns_empty_for_empty_string(self): | ||
| """Empty string returns an empty list.""" | ||
| tickets = find_asana_tickets("") | ||
| assert tickets == [] | ||
|
|
||
| def test_ignores_github_urls(self): | ||
| """GitHub issue URLs should not be mistaken for Asana tickets.""" | ||
| text = "Fix https://github.com/owner/repo/issues/42" | ||
| tickets = find_asana_tickets(text) | ||
| assert tickets == [] | ||
|
|
||
| def test_tickets_are_sorted(self): | ||
| """Returned list should be sorted alphabetically.""" | ||
| text = ( | ||
| "https://app.asana.com/0/2/222222222222" | ||
| " https://app.asana.com/0/1/111111111111" | ||
| ) | ||
| tickets = find_asana_tickets(text) | ||
| assert tickets == sorted(tickets) | ||
|
|
||
| def test_tickets_in_pr_description_mixed_content(self): | ||
| """Asana tickets mixed with other content in a PR description.""" | ||
| text = """## Summary | ||
| Related to https://app.asana.com/0/99/888888888888 | ||
| and https://app.asana.com/0/77/777777777777 | ||
|
|
||
| Also see GitHub issue #42 | ||
| """ | ||
| tickets = find_asana_tickets(text) | ||
| assert len(tickets) == 2 | ||
|
|
||
| async def test_extract_tickets_includes_asana_reference(self): | ||
| """extract_tickets() should include Asana references in ticket content.""" | ||
| provider = _GithubProvider( | ||
| "Related Asana task: https://app.asana.com/0/99/888888888888" | ||
| ) | ||
|
|
||
| tickets = await extract_tickets(provider) | ||
|
|
||
| assert tickets == [ | ||
| { | ||
| "ticket_id": "https://app.asana.com/0/99/888888888888", | ||
| "ticket_url": "https://app.asana.com/0/99/888888888888", | ||
| "title": "Asana Task: https://app.asana.com/0/99/888888888888", | ||
| "body": ( | ||
| "Asana task referenced in PR description. " | ||
| "Fetch task details from Asana for full context." | ||
| ), | ||
| "labels": "", | ||
| } | ||
| ] | ||
|
|
||
| async def test_extract_tickets_reserves_slot_for_asana_when_truncated(self): | ||
| """Asana references should not be dropped when the ticket list is capped.""" | ||
| provider = _GithubProvider( | ||
| "Fixes #1 and #2 and #3. " | ||
| "Related Asana task: https://app.asana.com/0/99/888888888888" | ||
| ) | ||
|
|
||
| tickets = await extract_tickets(provider) | ||
|
|
||
| assert len(tickets) == 3 | ||
| asana_tickets = [ | ||
| ticket for ticket in tickets | ||
| if ticket["ticket_url"].startswith("https://app.asana.com/") | ||
| ] | ||
| assert len(asana_tickets) == 1 |
Uh oh!
There was an error while loading. Please reload this page.