Skip to content

Commit

Permalink
[Integration][Jira] Enhance Jira Sync with Users, Mapping Issues To U…
Browse files Browse the repository at this point in the history
…sers (#1196)

# Description

What -This PR 
- Adds support for synchronizing Jira users to Port
- Relate Jira users to Jira issues

Why - Enhance jira integration


How

- Added a new `jiraUser` blueprint in `blueprints.json` with user
properties:
- Updated `client.py` to include `_get_paginated_users()` method:
- Implements pagination for user retrieval
- Handles cases like zero users
- Logs retrieval progress
- Modified `main.py` to add a new `on_resync_users()` handler:


## Type of change

Please leave one option from the following and delete the rest:

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] New Integration (non-breaking change which adds a new integration)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Non-breaking change (fix of existing functionality that will not
change current behavior)
- [ ] Documentation (added/updated documentation)

<h4> All tests should be run against the port production
environment(using a testing org). </h4>

### Core testing checklist

- [ ] Integration able to create all default resources from scratch
- [ ] Resync finishes successfully
- [ ] Resync able to create entities
- [ ] Resync able to update entities
- [ ] Resync able to detect and delete entities
- [ ] Scheduled resync able to abort existing resync and start a new one
- [ ] Tested with at least 2 integrations from scratch
- [ ] Tested with Kafka and Polling event listeners
- [ ] Tested deletion of entities that don't pass the selector


### Integration testing checklist

- [ ] Integration able to create all default resources from scratch
- [ ] Resync able to create entities
- [ ] Resync able to update entities
- [ ] Resync able to detect and delete entities
- [ ] Resync finishes successfully
- [ ] If new resource kind is added or updated in the integration, add
example raw data, mapping and expected result to the `examples` folder
in the integration directory.
- [ ] If resource kind is updated, run the integration with the example
data and check if the expected result is achieved
- [ ] If new resource kind is added or updated, validate that
live-events for that resource are working as expected
- [ ] Docs PR link [here](#)

### Preflight checklist

- [ ] Handled rate limiting
- [ ] Handled pagination
- [ ] Implemented the code in async
- [ ] Support Multi account

## Screenshots


![jiraIssue](https://github.com/user-attachments/assets/6e482101-44b8-410e-8081-8d7f0903d6e8)

![jiraUser](https://github.com/user-attachments/assets/199bca61-a45c-4d64-bb47-ec8d6f0ed381)



## API Documentation

Provide links to the API documentation used for this integration.

---------

Co-authored-by: PagesCoffy <[email protected]>
Co-authored-by: Tom Tankilevitch <[email protected]>
  • Loading branch information
3 people authored Dec 5, 2024
1 parent 209f328 commit 2f929d9
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 25 deletions.
77 changes: 65 additions & 12 deletions integrations/jira/.port/resources/blueprints.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,57 @@
},
"calculationProperties": {}
},
{
"identifier": "jiraUser",
"title": "Jira User",
"icon": "User",
"description": "A Jira user account",
"schema": {
"properties": {
"emailAddress": {
"title": "Email",
"type": "string",
"format": "email",
"description": "User's email address"
},
"displayName": {
"title": "Display Name",
"type": "string",
"description": "User's full name as displayed in Jira"
},
"active": {
"title": "Active Status",
"type": "boolean",
"description": "Whether the user account is active"
},
"accountType": {
"title": "Account Type",
"type": "string",
"description": "Type of Jira account (e.g., atlassian, customer)"
},
"timeZone": {
"title": "Time Zone",
"type": "string",
"description": "User's configured time zone"
},
"locale": {
"title": "Locale",
"type": "string",
"description": "User's configured locale"
},
"avatarUrl": {
"title": "Avatar URL",
"type": "string",
"format": "url",
"description": "URL for user's 48x48 avatar image"
}
}
},
"mirrorProperties": {},
"calculationProperties": {},
"aggregationProperties": {},
"relations": {}
},
{
"identifier": "jiraIssue",
"title": "Jira Issue",
Expand Down Expand Up @@ -43,18 +94,6 @@
"type": "array",
"description": "The components related to this issue"
},
"assignee": {
"title": "Assignee",
"type": "string",
"format": "user",
"description": "The user assigned to the issue"
},
"reporter": {
"title": "Reporter",
"type": "string",
"description": "The user that reported to the issue",
"format": "user"
},
"creator": {
"title": "Creator",
"type": "string",
Expand Down Expand Up @@ -102,6 +141,8 @@
"type": "number"
}
},
"mirrorProperties": {},
"aggregationProperties": {},
"relations": {
"project": {
"target": "jiraProject",
Expand All @@ -121,6 +162,18 @@
"title": "Subtasks",
"required": false,
"many": true
},
"assignee": {
"target": "jiraUser",
"title": "Assignee",
"required": false,
"many": false
},
"reporter": {
"target": "jiraUser",
"title": "Reporter",
"required": false,
"many": false
}
}
}
Expand Down
24 changes: 22 additions & 2 deletions integrations/jira/.port/resources/port-app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,25 @@ resources:
blueprint: '"jiraProject"'
properties:
url: (.self | split("/") | .[:3] | join("/")) + "/projects/" + .key

- kind: user
selector:
query: "true"
port:
entity:
mappings:
identifier: .accountId
title: .displayName
blueprint: '"jiraUser"'
properties:
emailAddress: .emailAddress
displayName: .displayName
active: .active
accountType: .accountType
timeZone: .timeZone
locale: .locale
avatarUrl: .avatarUrls["48x48"]

- kind: issue
selector:
query: "true"
Expand All @@ -27,8 +46,6 @@ resources:
status: .fields.status.name
issueType: .fields.issuetype.name
components: .fields.components
assignee: .fields.assignee.emailAddress
reporter: .fields.reporter.emailAddress
creator: .fields.creator.emailAddress
priority: .fields.priority.id
labels: .fields.labels
Expand All @@ -39,3 +56,6 @@ resources:
project: .fields.project.key
parentIssue: .fields.parent.key
subtasks: .fields.subtasks | map(.key)
assignee: .fields.assignee.accountId
reporter: .fields.reporter.accountId

1 change: 1 addition & 0 deletions integrations/jira/.port/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ features:
resources:
- kind: project
- kind: issue
- kind: user
configurations:
- name: appHost
required: false
Expand Down
6 changes: 6 additions & 0 deletions integrations/jira/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

<!-- towncrier release notes start -->
## 0.2.1 (2024-12-05)


### Improvements

- Added support to sync Jira users to Port and created relevant relations to jira issues assignee and reporter

## 0.2.0 (2024-12-04)

Expand Down
47 changes: 47 additions & 0 deletions integrations/jira/jira/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"project_restored_deleted",
"project_archived",
"project_restored_archived",
"user_created",
"user_updated",
"user_deleted",
]


Expand Down Expand Up @@ -78,6 +81,11 @@ async def _get_paginated_issues(self, params: dict[str, Any]) -> dict[str, Any]:
issue_response.raise_for_status()
return issue_response.json()

async def _get_users_data(self, params: dict[str, Any]) -> list[dict[str, Any]]:
user_response = await self.client.get(f"{self.api_url}/users", params=params)
user_response.raise_for_status()
return user_response.json()

async def create_events_webhook(self, app_host: str) -> None:
webhook_target_app_host = f"{app_host}/integration/webhook"
webhook_check_response = await self.client.get(f"{self.webhooks_url}")
Expand Down Expand Up @@ -160,3 +168,42 @@ async def get_paginated_issues(self) -> AsyncGenerator[list[dict[str, Any]], Non
issue_response_list = (await self._get_paginated_issues(params))["issues"]
yield issue_response_list
params["startAt"] += PAGE_SIZE

async def get_paginated_users(
self,
) -> AsyncGenerator[list[dict[str, Any]], None]:
logger.info("Getting users from Jira")

params = self._generate_base_req_params()

total_users = len(await self._get_users_data(params))

if total_users == 0:
logger.warning(
"User query returned 0 users, did you provide the correct Jira API credentials?"
)

params["maxResults"] = PAGE_SIZE
while params["startAt"] < total_users:
logger.info(f"Current query position: {params['startAt']}/{total_users}")

user_response_list = await self._get_users_data(params)

if not user_response_list:
logger.warning(f"No users found at {params['startAt']}")
break

logger.info(
f"Retrieved users: {len(user_response_list)} "
f"(Position: {params['startAt']}/{total_users})"
)

yield user_response_list
params["startAt"] += PAGE_SIZE

async def get_single_user(self, account_id: str) -> dict[str, Any]:
user_response = await self.client.get(
f"{self.api_url}/user", params={"accountId": account_id}
)
user_response.raise_for_status()
return user_response.json()
69 changes: 59 additions & 10 deletions integrations/jira/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
class ObjectKind(StrEnum):
PROJECT = "project"
ISSUE = "issue"
USER = "user"


async def setup_application() -> None:
Expand Down Expand Up @@ -59,23 +60,71 @@ async def on_resync_issues(kind: str) -> ASYNC_GENERATOR_RESYNC_TYPE:
yield issues


@ocean.on_resync(ObjectKind.USER)
async def on_resync_users(kind: str) -> ASYNC_GENERATOR_RESYNC_TYPE:
client = JiraClient(
ocean.integration_config["jira_host"],
ocean.integration_config["atlassian_user_email"],
ocean.integration_config["atlassian_user_token"],
)

async for users in client.get_paginated_users():
logger.info(f"Received users batch with {len(users)} users")
yield users


@ocean.router.post("/webhook")
async def handle_webhook_request(data: dict[str, Any]) -> dict[str, Any]:
client = JiraClient(
ocean.integration_config["jira_host"],
ocean.integration_config["atlassian_user_email"],
ocean.integration_config["atlassian_user_token"],
)
logger.info(f'Received webhook event of type: {data.get("webhookEvent")}')
if "project" in data:
logger.info(f'Received webhook event for project: {data["project"]["key"]}')
project = await client.get_single_project(data["project"]["key"])
await ocean.register_raw(ObjectKind.PROJECT, [project])
elif "issue" in data:
logger.info(f'Received webhook event for issue: {data["issue"]["key"]}')
issue = await client.get_single_issue(data["issue"]["key"])
await ocean.register_raw(ObjectKind.ISSUE, [issue])
logger.info("Webhook event processed")

webhook_event = data.get("webhookEvent")
if not webhook_event:
logger.error("Missing webhook event")
return {"ok": False, "error": "Missing webhook event"}

logger.info(f"Processing webhook event: {webhook_event}")

match webhook_event:
case event if event.startswith("user_"):
account_id = data["user"]["accountId"]
logger.debug(f"Fetching user with accountId: {account_id}")
item = await client.get_single_user(account_id)
kind = ObjectKind.USER
case event if event.startswith("project_"):
project_key = data["project"]["key"]
logger.debug(f"Fetching project with key: {project_key}")
item = await client.get_single_project(project_key)
kind = ObjectKind.PROJECT
case event if event.startswith("jira:issue_"):
issue_key = data["issue"]["key"]
logger.debug(f"Fetching issue with key: {issue_key}")
item = await client.get_single_issue(issue_key)
kind = ObjectKind.ISSUE
case _:
logger.error(f"Unknown webhook event type: {webhook_event}")
return {
"ok": False,
"error": f"Unknown webhook event type: {webhook_event}",
}

if not item:
logger.error("Failed to retrieve item")
return {"ok": False, "error": "Failed to retrieve item"}

logger.debug(f"Retrieved {kind} item: {item}")

if "deleted" in webhook_event:
logger.info(f"Unregistering {kind} item")
await ocean.unregister_raw(kind, [item])
else:
logger.info(f"Registering {kind} item")
await ocean.register_raw(kind, [item])

logger.info(f"Webhook event '{webhook_event}' processed successfully")
return {"ok": True}


Expand Down
2 changes: 1 addition & 1 deletion integrations/jira/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "jira"
version = "0.2.0"
version = "0.2.1"
description = "Integration to bring information from Jira into Port"
authors = ["Mor Paz <[email protected]>"]

Expand Down

0 comments on commit 2f929d9

Please sign in to comment.