feat(phase-35): Jira + Tempo integration with internal worklogs
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

Full Jira/Tempo pipeline: link Aegis entities to Jira issues, auto-sync
status hourly, log time internally with integrity hashing, and optionally
push worklogs to Tempo.

- 1.1 JiraLink model + Worklog model: Alembic migration b020 with indexes,
  enums (jiralinkentitytype, jirasyncdirection), and integrity_hash column
- 1.2 Jira service: atlassian-python-api wrapper with lazy singleton client,
  search/create/sync operations, feature-flagged via JIRA_ENABLED
- 1.3 Jira router: CRUD endpoints for /jira/links, /jira/search,
  /jira/create-issue with audit logging and entity-to-issue auto-creation
- 1.4 Tempo service: worklog push via tempo-api-python-client, auto-log from
  test completions when TEMPO_ENABLED, graceful fallback on failure
- 1.5 Worklog service + router: immutable internal time records with SHA-256
  integrity hash, CRUD at /worklogs, /worklogs/{id}/verify endpoint
- 1.6 Frontend: JiraLinkPanel component (search, link, sync, unlink) and
  WorklogTimeline component (timeline view, manual log form) integrated into
  TestDetailPage sidebar, CampaignDetailPage grid, TechniqueDetailPage
- 1.7 Jira sync job: APScheduler hourly job syncs all links from Jira,
  registered in background scheduler alongside existing jobs
This commit is contained in:
2026-02-17 15:57:39 +01:00
parent 6d18a5417d
commit 9b98f60a9a
23 changed files with 1605 additions and 1 deletions

View File

@@ -0,0 +1,57 @@
"""Jira integration models — link Aegis entities to Jira issues."""
import enum
import uuid
from datetime import datetime
from sqlalchemy import Column, String, DateTime, ForeignKey, Enum as SQLEnum, Index
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship
from app.database import Base
class JiraLinkEntityType(str, enum.Enum):
test = "test"
technique = "technique"
campaign = "campaign"
evidence = "evidence"
class JiraSyncDirection(str, enum.Enum):
aegis_to_jira = "aegis_to_jira"
jira_to_aegis = "jira_to_aegis"
bidirectional = "bidirectional"
class JiraLink(Base):
"""Associates an Aegis entity with a Jira issue for bidirectional sync."""
__tablename__ = "jira_links"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
entity_type = Column(SQLEnum(JiraLinkEntityType), nullable=False)
entity_id = Column(UUID(as_uuid=True), nullable=False)
jira_issue_key = Column(String(50), nullable=False)
jira_issue_id = Column(String(50))
jira_project_key = Column(String(20))
jira_status = Column(String(100))
jira_priority = Column(String(50))
jira_assignee = Column(String(255))
jira_story_points = Column(String(10))
sync_direction = Column(
SQLEnum(JiraSyncDirection), default=JiraSyncDirection.bidirectional
)
last_synced_at = Column(DateTime)
sync_metadata = Column(JSONB, default={})
created_by = Column(UUID(as_uuid=True), ForeignKey("users.id"))
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
creator = relationship("User", foreign_keys=[created_by])
__table_args__ = (
Index("ix_jira_links_entity_id", "entity_id"),
Index("ix_jira_links_issue_key", "jira_issue_key"),
Index("ix_jira_links_entity_type_entity_id", "entity_type", "entity_id"),
)