Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions backend/alembic/versions/095_add_jira_credential_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""add jira credential type

Revision ID: 095_add_jira_credential_type
Revises: 093_add_codex_node_support
Create Date: 2026-07-06 00:00:00.000000
"""

from collections.abc import Sequence

from alembic import op

revision: str = "095_add_jira_credential_type"
down_revision: str | Sequence[str] | None = "093_add_codex_node_support"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
op.execute("ALTER TYPE credential_type ADD VALUE IF NOT EXISTS 'jira'")


def downgrade() -> None:
# PostgreSQL enum values cannot be removed safely without rebuilding the type.
pass
24 changes: 24 additions & 0 deletions backend/alembic/versions/096_merge_jira_file_heads.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Merge Jira and file team share migration heads.

Revision ID: 096_merge_jira_file_heads
Revises: 095_add_jira_credential_type, 094_add_file_team_shares
Create Date: 2026-07-09 00:00:00.000000
"""

from collections.abc import Sequence

revision: str = "096_merge_jira_file_heads"
down_revision: tuple[str, str] = (
"095_add_jira_credential_type",
"094_add_file_team_shares",
)
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Merge the Jira and file team share migration branches."""


def downgrade() -> None:
"""Split the migration graph back into its parent branches."""
69 changes: 69 additions & 0 deletions backend/app/api/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ def merge_credential_config_for_update(

return merged_config

if credential_type == CredentialType.jira:
merged_config = dict(existing_config)
for key in ("email", "api_token", "base_url", "api_version", "deployment"):
incoming_value = str(incoming_config.get(key, "") or "").strip()
if incoming_value:
merged_config[key] = incoming_value
return merged_config

if credential_type != CredentialType.github:
return incoming_config

Expand Down Expand Up @@ -181,6 +189,9 @@ def get_masked_value(credential_type: CredentialType, config: dict) -> str | Non
):
api_key = config.get("api_key", "")
return mask_api_key(api_key)
elif credential_type == CredentialType.jira:
api_token = str(config.get("api_token", "")).strip()
return mask_api_key(api_token)
elif credential_type == CredentialType.qdrant:
openai_api_key = config.get("openai_api_key", "")
return mask_api_key(openai_api_key)
Expand Down Expand Up @@ -274,6 +285,17 @@ def get_public_credential_fields(
if credential_type == CredentialType.linear:
auth_mode = str(config.get("auth_mode", "api_key")).strip() or "api_key"
return {"auth_mode": auth_mode}
if credential_type == CredentialType.jira:
base_url = str(config.get("base_url", "")).strip() or None
email = str(config.get("email", "")).strip() or None
api_version = str(config.get("api_version", "") or "").strip() or None
deployment = str(config.get("deployment", "") or "").strip() or "cloud"
return {
"base_url": base_url,
"email": email,
"api_version": api_version,
"deployment": deployment,
}
if credential_type == CredentialType.codex:
auth_mode = str(config.get("auth_mode", "access_token")).strip() or "access_token"
fields: dict[str, str | None] = {"auth_mode": auth_mode}
Expand Down Expand Up @@ -799,6 +821,7 @@ async def run_credential_connection_test(
"""Test whether a credential configuration can reach the external service."""
if test_data.type not in {
CredentialType.supabase,
CredentialType.jira,
CredentialType.linear,
CredentialType.notion,
CredentialType.clickhouse,
Expand Down Expand Up @@ -827,6 +850,8 @@ async def run_credential_connection_test(
config = _merge_supabase_test_config(config, stored_config)
elif test_data.type == CredentialType.linear:
config = _merge_linear_test_config(config, stored_config)
elif test_data.type == CredentialType.jira:
config = {**stored_config, **{k: v for k, v in config.items() if v}}
elif test_data.type == CredentialType.clickhouse:
config = _merge_clickhouse_test_config(config, stored_config)
elif test_data.type == CredentialType.sentry:
Expand Down Expand Up @@ -861,6 +886,21 @@ async def run_credential_connection_test(
)
return CredentialTestResponse(success=True, message="Connection successful")

if test_data.type == CredentialType.jira:
from app.services.jira_service import JiraService

service = JiraService(config)
try:
user = service.test_connection()
finally:
service.close()
user_name = str(
user.get("displayName") or user.get("emailAddress") or user.get("accountId") or ""
)
if user_name:
return CredentialTestResponse(success=True, message=f"Connected as {user_name}")
return CredentialTestResponse(success=True, message="Connection successful")

if test_data.type == CredentialType.clickhouse:
from app.services.clickhouse_service import ClickHouseService

Expand Down Expand Up @@ -1332,6 +1372,35 @@ def validate_credential_config(
status_code=status.HTTP_400_BAD_REQUEST,
detail="GitHub credential base_url must be a valid http(s) URL",
)
elif credential_type == CredentialType.jira:
if "email" not in config or not str(config["email"]).strip():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Jira credential requires email",
)
if "api_token" not in config or not str(config["api_token"]).strip():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Jira credential requires api_token",
)
jira_base_url = str(config.get("base_url", "") or "").strip()
if not jira_base_url:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Jira credential requires base_url",
)
parsed = urlparse(jira_base_url)
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Jira credential base_url must be a valid http(s) URL",
)
deployment = str(config.get("deployment", "") or "cloud").strip()
if deployment not in {"cloud", "data_center"}:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Jira credential deployment must be cloud or data_center",
)
elif credential_type == CredentialType.sentry:
if "api_token" not in config or not str(config["api_token"]).strip():
raise HTTPException(
Expand Down
1 change: 1 addition & 0 deletions backend/app/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class CredentialType(str, PyEnum):
codex = "codex"
google = "google"
github = "github"
jira = "jira"
linear = "linear"
custom = "custom"
bearer = "bearer"
Expand Down
9 changes: 9 additions & 0 deletions backend/app/models/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ class CredentialType(str, Enum):
codex = "codex"
google = "google"
github = "github"
jira = "jira"
linear = "linear"
custom = "custom"
bearer = "bearer"
Expand Down Expand Up @@ -518,6 +519,14 @@ class CredentialConfigGitHub(BaseModel):
base_url: str | None = None


class CredentialConfigJira(BaseModel):
email: str
api_token: str
base_url: str
deployment: str | None = None
api_version: str | None = None


class CredentialConfigLinear(BaseModel):
api_key: str | None = None
auth_mode: str | None = None
Expand Down
Loading
Loading