2026-03-26 21:57:56 +00:00
|
|
|
"""Temporal activity definitions for activity-core.
|
|
|
|
|
|
|
|
|
|
Activities run inside a Worker bound to 'orchestrator-tq'.
|
|
|
|
|
Each function is decorated with @activity.defn and executed by
|
|
|
|
|
RunActivityWorkflow via workflow.execute_activity().
|
|
|
|
|
|
2026-03-26 22:02:15 +00:00
|
|
|
DB access pattern: worker.py calls init_session_factory(url) once before
|
|
|
|
|
starting workers, which sets the module-level _session_factory used by
|
|
|
|
|
activities that need DB access.
|
2026-03-26 21:57:56 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-03-26 22:02:15 +00:00
|
|
|
import uuid
|
2026-03-26 22:19:12 +00:00
|
|
|
from datetime import datetime, timezone
|
2026-03-26 22:02:15 +00:00
|
|
|
|
|
|
|
|
from sqlalchemy import select
|
2026-03-26 22:25:19 +00:00
|
|
|
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
2026-03-26 22:02:15 +00:00
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
2026-03-26 21:57:56 +00:00
|
|
|
from temporalio import activity
|
2026-03-26 22:02:15 +00:00
|
|
|
from temporalio.exceptions import ApplicationError
|
|
|
|
|
|
|
|
|
|
from activity_core.db import make_engine
|
2026-05-14 23:02:33 +02:00
|
|
|
from activity_core.issue_sink import get_issue_sink
|
2026-03-26 22:02:15 +00:00
|
|
|
from activity_core.orm import ActivityDefinition as ActivityDefinitionRow
|
2026-05-14 23:02:33 +02:00
|
|
|
from activity_core.orm import ActivityRun, TaskInstance, TaskSpawnLog
|
|
|
|
|
from activity_core.rules import evaluate_condition
|
2026-03-26 22:02:15 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
_session_factory: async_sessionmaker[AsyncSession] | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def init_session_factory(url: str) -> None:
|
|
|
|
|
"""Initialise the shared DB session factory.
|
|
|
|
|
|
|
|
|
|
Must be called once from worker.py before workers are started.
|
|
|
|
|
"""
|
|
|
|
|
global _session_factory
|
|
|
|
|
_session_factory = async_sessionmaker(make_engine(url), expire_on_commit=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_session_factory() -> async_sessionmaker[AsyncSession]:
|
|
|
|
|
if _session_factory is None:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
"DB session factory not initialised — call init_session_factory() first"
|
|
|
|
|
)
|
|
|
|
|
return _session_factory
|
|
|
|
|
|
2026-03-26 21:57:56 +00:00
|
|
|
|
2026-03-26 22:02:15 +00:00
|
|
|
# ── Activities ─────────────────────────────────────────────────────────────────
|
2026-03-26 21:57:56 +00:00
|
|
|
|
|
|
|
|
@activity.defn
|
|
|
|
|
async def load_activity_definition(activity_id: str) -> dict:
|
2026-03-26 22:02:15 +00:00
|
|
|
"""Load an ActivityDefinition row from Postgres by ID.
|
2026-03-26 21:57:56 +00:00
|
|
|
|
2026-03-26 22:02:15 +00:00
|
|
|
Returns a JSON-serialisable dict suitable for passing between
|
|
|
|
|
Temporal workflow steps.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
ApplicationError (non-retryable): if no row exists for activity_id.
|
2026-03-26 21:57:56 +00:00
|
|
|
"""
|
2026-03-26 22:02:15 +00:00
|
|
|
Session = _get_session_factory()
|
|
|
|
|
async with Session() as session:
|
|
|
|
|
row = await session.scalar(
|
|
|
|
|
select(ActivityDefinitionRow).where(
|
|
|
|
|
ActivityDefinitionRow.id == uuid.UUID(activity_id)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if row is None:
|
|
|
|
|
raise ApplicationError(
|
|
|
|
|
f"ActivityDefinition {activity_id!r} not found",
|
|
|
|
|
non_retryable=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"id": str(row.id),
|
|
|
|
|
"name": row.name,
|
|
|
|
|
"enabled": row.enabled,
|
|
|
|
|
"trigger_type": row.trigger_type,
|
|
|
|
|
"trigger_config": row.trigger_config,
|
|
|
|
|
"context_sources": row.context_sources,
|
|
|
|
|
"task_templates": row.task_templates,
|
2026-05-14 23:02:33 +02:00
|
|
|
"rules": row.rules_json,
|
|
|
|
|
"instructions": row.instructions_json,
|
2026-03-26 22:02:15 +00:00
|
|
|
"dedupe_key_strategy": row.dedupe_key_strategy,
|
|
|
|
|
"version": row.version,
|
|
|
|
|
}
|
2026-03-26 21:57:56 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@activity.defn
|
feat(WP-0003c): context adapters, first ActivityDefinition, full test suite
T51: ContextResolver ABC + CONTEXT_RESOLVER_REGISTRY; resolve_context activity
updated to dispatch via registry (warns + binds {} on failure, never aborts run).
T52: RepoScopingContextResolver with 5-min in-process cache.
T53: StateHubContextResolver (no cache) for domain_summary and repo_sbom_status.
T54: activity-definitions/weekly-sbom-staleness.md (Monday 09:00 Berlin, cron
trigger, flag-stale-sbom rule at >30 days) + tasks/sbom-rescan.md template.
T55: 51 parametrized evaluator tests — all whitelisted operators, unsafe
expression rejection, empty condition, missing attribute, nested context access.
T56: 15 executor safety tests — UntrustedFieldError, object-type rejection,
injection fixture, LLM retry on bad JSON, review_required field.
T57: 6 integration tests — parses real definition, evaluates rule per-repo
(stale/fresh boundary), emits via NullSink, verifies spawn log entries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:24:48 +02:00
|
|
|
async def resolve_context(
|
|
|
|
|
context_sources: list[dict],
|
|
|
|
|
event_envelope_json: str | None = None,
|
|
|
|
|
) -> dict:
|
2026-03-26 22:06:09 +00:00
|
|
|
"""Resolve each context source and merge into a snapshot dict.
|
2026-03-26 21:57:56 +00:00
|
|
|
|
feat(WP-0003c): context adapters, first ActivityDefinition, full test suite
T51: ContextResolver ABC + CONTEXT_RESOLVER_REGISTRY; resolve_context activity
updated to dispatch via registry (warns + binds {} on failure, never aborts run).
T52: RepoScopingContextResolver with 5-min in-process cache.
T53: StateHubContextResolver (no cache) for domain_summary and repo_sbom_status.
T54: activity-definitions/weekly-sbom-staleness.md (Monday 09:00 Berlin, cron
trigger, flag-stale-sbom rule at >30 days) + tasks/sbom-rescan.md template.
T55: 51 parametrized evaluator tests — all whitelisted operators, unsafe
expression rejection, empty condition, missing attribute, nested context access.
T56: 15 executor safety tests — UntrustedFieldError, object-type rejection,
injection fixture, LLM retry on bad JSON, review_required field.
T57: 6 integration tests — parses real definition, evaluates rule per-repo
(stale/fresh boundary), emits via NullSink, verifies spawn log entries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:24:48 +02:00
|
|
|
Returns: {bind_key: resolved_value, ...}
|
2026-03-26 22:06:09 +00:00
|
|
|
|
feat(WP-0003c): context adapters, first ActivityDefinition, full test suite
T51: ContextResolver ABC + CONTEXT_RESOLVER_REGISTRY; resolve_context activity
updated to dispatch via registry (warns + binds {} on failure, never aborts run).
T52: RepoScopingContextResolver with 5-min in-process cache.
T53: StateHubContextResolver (no cache) for domain_summary and repo_sbom_status.
T54: activity-definitions/weekly-sbom-staleness.md (Monday 09:00 Berlin, cron
trigger, flag-stale-sbom rule at >30 days) + tasks/sbom-rescan.md template.
T55: 51 parametrized evaluator tests — all whitelisted operators, unsafe
expression rejection, empty condition, missing attribute, nested context access.
T56: 15 executor safety tests — UntrustedFieldError, object-type rejection,
injection fixture, LLM retry on bad JSON, review_required field.
T57: 6 integration tests — parses real definition, evaluates rule per-repo
(stale/fresh boundary), emits via NullSink, verifies spawn log entries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:24:48 +02:00
|
|
|
Source types are dispatched via CONTEXT_RESOLVER_REGISTRY.
|
|
|
|
|
A resolver that raises logs a warning and binds {} — it does not abort the run.
|
|
|
|
|
The 'static' type is handled inline without a registry entry.
|
2026-03-26 21:57:56 +00:00
|
|
|
"""
|
feat(WP-0003c): context adapters, first ActivityDefinition, full test suite
T51: ContextResolver ABC + CONTEXT_RESOLVER_REGISTRY; resolve_context activity
updated to dispatch via registry (warns + binds {} on failure, never aborts run).
T52: RepoScopingContextResolver with 5-min in-process cache.
T53: StateHubContextResolver (no cache) for domain_summary and repo_sbom_status.
T54: activity-definitions/weekly-sbom-staleness.md (Monday 09:00 Berlin, cron
trigger, flag-stale-sbom rule at >30 days) + tasks/sbom-rescan.md template.
T55: 51 parametrized evaluator tests — all whitelisted operators, unsafe
expression rejection, empty condition, missing attribute, nested context access.
T56: 15 executor safety tests — UntrustedFieldError, object-type rejection,
injection fixture, LLM retry on bad JSON, review_required field.
T57: 6 integration tests — parses real definition, evaluates rule per-repo
(stale/fresh boundary), emits via NullSink, verifies spawn log entries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:24:48 +02:00
|
|
|
import activity_core.context_resolvers # noqa: F401 — registers all adapters
|
|
|
|
|
from activity_core.context_resolvers.base import CONTEXT_RESOLVER_REGISTRY
|
|
|
|
|
|
2026-03-26 22:06:09 +00:00
|
|
|
snapshot: dict = {}
|
|
|
|
|
for source in context_sources:
|
feat(WP-0003c): context adapters, first ActivityDefinition, full test suite
T51: ContextResolver ABC + CONTEXT_RESOLVER_REGISTRY; resolve_context activity
updated to dispatch via registry (warns + binds {} on failure, never aborts run).
T52: RepoScopingContextResolver with 5-min in-process cache.
T53: StateHubContextResolver (no cache) for domain_summary and repo_sbom_status.
T54: activity-definitions/weekly-sbom-staleness.md (Monday 09:00 Berlin, cron
trigger, flag-stale-sbom rule at >30 days) + tasks/sbom-rescan.md template.
T55: 51 parametrized evaluator tests — all whitelisted operators, unsafe
expression rejection, empty condition, missing attribute, nested context access.
T56: 15 executor safety tests — UntrustedFieldError, object-type rejection,
injection fixture, LLM retry on bad JSON, review_required field.
T57: 6 integration tests — parses real definition, evaluates rule per-repo
(stale/fresh boundary), emits via NullSink, verifies spawn log entries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:24:48 +02:00
|
|
|
source_type = source.get("type", "")
|
|
|
|
|
query = source.get("query", "")
|
|
|
|
|
params = source.get("params") or {}
|
|
|
|
|
raw_bind = source.get("bind_to") or source.get("name") or source_type
|
|
|
|
|
# Strip the 'context.' namespace prefix so evaluator can find the key.
|
|
|
|
|
bind_key = raw_bind.removeprefix("context.") if raw_bind.startswith("context.") else raw_bind
|
2026-03-26 22:06:09 +00:00
|
|
|
|
|
|
|
|
if source_type == "static":
|
feat(WP-0003c): context adapters, first ActivityDefinition, full test suite
T51: ContextResolver ABC + CONTEXT_RESOLVER_REGISTRY; resolve_context activity
updated to dispatch via registry (warns + binds {} on failure, never aborts run).
T52: RepoScopingContextResolver with 5-min in-process cache.
T53: StateHubContextResolver (no cache) for domain_summary and repo_sbom_status.
T54: activity-definitions/weekly-sbom-staleness.md (Monday 09:00 Berlin, cron
trigger, flag-stale-sbom rule at >30 days) + tasks/sbom-rescan.md template.
T55: 51 parametrized evaluator tests — all whitelisted operators, unsafe
expression rejection, empty condition, missing attribute, nested context access.
T56: 15 executor safety tests — UntrustedFieldError, object-type rejection,
injection fixture, LLM retry on bad JSON, review_required field.
T57: 6 integration tests — parses real definition, evaluates rule per-repo
(stale/fresh boundary), emits via NullSink, verifies spawn log entries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:24:48 +02:00
|
|
|
snapshot[bind_key] = source.get("config", {}).get("value")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
resolver_cls = CONTEXT_RESOLVER_REGISTRY.get(source_type)
|
|
|
|
|
if resolver_cls is None:
|
|
|
|
|
activity.logger.warning(
|
|
|
|
|
"Unknown context source type %r — binding {}",
|
|
|
|
|
source_type,
|
2026-03-26 22:06:09 +00:00
|
|
|
)
|
feat(WP-0003c): context adapters, first ActivityDefinition, full test suite
T51: ContextResolver ABC + CONTEXT_RESOLVER_REGISTRY; resolve_context activity
updated to dispatch via registry (warns + binds {} on failure, never aborts run).
T52: RepoScopingContextResolver with 5-min in-process cache.
T53: StateHubContextResolver (no cache) for domain_summary and repo_sbom_status.
T54: activity-definitions/weekly-sbom-staleness.md (Monday 09:00 Berlin, cron
trigger, flag-stale-sbom rule at >30 days) + tasks/sbom-rescan.md template.
T55: 51 parametrized evaluator tests — all whitelisted operators, unsafe
expression rejection, empty condition, missing attribute, nested context access.
T56: 15 executor safety tests — UntrustedFieldError, object-type rejection,
injection fixture, LLM retry on bad JSON, review_required field.
T57: 6 integration tests — parses real definition, evaluates rule per-repo
(stale/fresh boundary), emits via NullSink, verifies spawn log entries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:24:48 +02:00
|
|
|
snapshot[bind_key] = {}
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
snapshot[bind_key] = resolver_cls().resolve(query, None, params)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
activity.logger.warning(
|
|
|
|
|
"Context resolver %r failed — %s; binding {}",
|
|
|
|
|
source_type,
|
|
|
|
|
exc,
|
2026-03-26 22:06:09 +00:00
|
|
|
)
|
feat(WP-0003c): context adapters, first ActivityDefinition, full test suite
T51: ContextResolver ABC + CONTEXT_RESOLVER_REGISTRY; resolve_context activity
updated to dispatch via registry (warns + binds {} on failure, never aborts run).
T52: RepoScopingContextResolver with 5-min in-process cache.
T53: StateHubContextResolver (no cache) for domain_summary and repo_sbom_status.
T54: activity-definitions/weekly-sbom-staleness.md (Monday 09:00 Berlin, cron
trigger, flag-stale-sbom rule at >30 days) + tasks/sbom-rescan.md template.
T55: 51 parametrized evaluator tests — all whitelisted operators, unsafe
expression rejection, empty condition, missing attribute, nested context access.
T56: 15 executor safety tests — UntrustedFieldError, object-type rejection,
injection fixture, LLM retry on bad JSON, review_required field.
T57: 6 integration tests — parses real definition, evaluates rule per-repo
(stale/fresh boundary), emits via NullSink, verifies spawn log entries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:24:48 +02:00
|
|
|
snapshot[bind_key] = {}
|
2026-03-26 22:06:09 +00:00
|
|
|
|
|
|
|
|
return snapshot
|
2026-03-26 21:57:56 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@activity.defn
|
|
|
|
|
async def log_run(run_payload: dict) -> str:
|
2026-03-26 22:19:12 +00:00
|
|
|
"""Persist an ActivityRun record to Postgres and return its run_id.
|
2026-03-26 21:57:56 +00:00
|
|
|
|
2026-03-26 22:25:19 +00:00
|
|
|
Idempotent: uses INSERT … ON CONFLICT (run_id) DO NOTHING so Temporal
|
|
|
|
|
activity retries do not produce duplicate rows.
|
|
|
|
|
|
2026-03-26 22:19:12 +00:00
|
|
|
Expected keys in run_payload:
|
2026-03-26 22:25:19 +00:00
|
|
|
run_id (str UUID — computed deterministically in workflow)
|
2026-03-26 22:19:12 +00:00
|
|
|
activity_id (str UUID)
|
|
|
|
|
scheduled_for (ISO-8601 str or None)
|
|
|
|
|
context_snapshot (dict)
|
|
|
|
|
tasks_spawned (int)
|
|
|
|
|
version_used (int)
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
run_id as a str UUID.
|
2026-03-26 21:57:56 +00:00
|
|
|
"""
|
2026-03-26 22:19:12 +00:00
|
|
|
Session = _get_session_factory()
|
|
|
|
|
|
2026-03-26 22:25:19 +00:00
|
|
|
run_id = uuid.UUID(run_payload["run_id"])
|
|
|
|
|
|
2026-03-26 22:19:12 +00:00
|
|
|
scheduled_for: datetime | None = None
|
|
|
|
|
if run_payload.get("scheduled_for"):
|
|
|
|
|
scheduled_for = datetime.fromisoformat(run_payload["scheduled_for"])
|
|
|
|
|
|
2026-03-26 22:25:19 +00:00
|
|
|
stmt = (
|
|
|
|
|
pg_insert(ActivityRun)
|
|
|
|
|
.values(
|
|
|
|
|
run_id=run_id,
|
|
|
|
|
activity_id=uuid.UUID(run_payload["activity_id"]),
|
|
|
|
|
scheduled_for=scheduled_for,
|
|
|
|
|
fired_at=datetime.now(tz=timezone.utc),
|
|
|
|
|
context_snapshot=run_payload["context_snapshot"],
|
|
|
|
|
tasks_spawned=run_payload["tasks_spawned"],
|
|
|
|
|
version_used=run_payload["version_used"],
|
|
|
|
|
)
|
|
|
|
|
.on_conflict_do_nothing(index_elements=["run_id"])
|
2026-03-26 22:19:12 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async with Session() as session:
|
|
|
|
|
async with session.begin():
|
2026-03-26 22:25:19 +00:00
|
|
|
await session.execute(stmt)
|
2026-03-26 22:19:12 +00:00
|
|
|
|
2026-03-26 22:25:19 +00:00
|
|
|
return str(run_id)
|
2026-03-26 22:30:50 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@activity.defn
|
|
|
|
|
async def persist_task_instance(task_payload: dict) -> str:
|
|
|
|
|
"""Write a TaskInstance row and return its id.
|
|
|
|
|
|
|
|
|
|
Idempotent: uses INSERT … ON CONFLICT (id) DO NOTHING.
|
|
|
|
|
|
|
|
|
|
Expected keys in task_payload:
|
|
|
|
|
id (str UUID — deterministic, computed in TaskExecutorWorkflow)
|
|
|
|
|
run_id (str UUID)
|
|
|
|
|
type (str)
|
|
|
|
|
params (dict)
|
|
|
|
|
status (str, default "done" for stub)
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
task instance id as a str UUID.
|
|
|
|
|
"""
|
|
|
|
|
Session = _get_session_factory()
|
|
|
|
|
task_id = uuid.UUID(task_payload["id"])
|
|
|
|
|
|
|
|
|
|
stmt = (
|
|
|
|
|
pg_insert(TaskInstance)
|
|
|
|
|
.values(
|
|
|
|
|
id=task_id,
|
|
|
|
|
run_id=uuid.UUID(task_payload["run_id"]),
|
|
|
|
|
type=task_payload["type"],
|
|
|
|
|
params=task_payload.get("params", {}),
|
|
|
|
|
status=task_payload.get("status", "done"),
|
|
|
|
|
)
|
|
|
|
|
.on_conflict_do_nothing(index_elements=["id"])
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async with Session() as session:
|
|
|
|
|
async with session.begin():
|
|
|
|
|
await session.execute(stmt)
|
|
|
|
|
|
|
|
|
|
return str(task_id)
|
2026-05-14 23:02:33 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@activity.defn
|
|
|
|
|
async def evaluate_rules(payload: dict) -> list[dict]:
|
|
|
|
|
"""Evaluate each rule condition against the event and context.
|
|
|
|
|
|
|
|
|
|
Returns the list of matching rule dicts (those whose condition is True).
|
|
|
|
|
Rules that raise UnsafeExpression or any other error are skipped and logged.
|
|
|
|
|
|
|
|
|
|
Expected keys in payload:
|
|
|
|
|
rules list[dict] — RuleDef serialised dicts
|
|
|
|
|
event dict — EventEnvelope attributes (or empty for cron)
|
|
|
|
|
context dict — context snapshot from resolve_context
|
|
|
|
|
"""
|
|
|
|
|
from activity_core.rules.evaluator import UnsafeExpression
|
|
|
|
|
|
|
|
|
|
rules = payload.get("rules", [])
|
|
|
|
|
event_attrs = payload.get("event", {})
|
|
|
|
|
context = payload.get("context", {})
|
|
|
|
|
|
|
|
|
|
# Build a simple object whose attributes mirror event fields for the evaluator.
|
|
|
|
|
class _Env:
|
|
|
|
|
def __init__(self, attrs: dict) -> None:
|
|
|
|
|
self.attributes = _DictObj(attrs)
|
|
|
|
|
|
|
|
|
|
class _DictObj:
|
|
|
|
|
def __init__(self, d: dict) -> None:
|
|
|
|
|
self.__dict__.update(d)
|
|
|
|
|
|
|
|
|
|
event_obj = _Env(event_attrs)
|
|
|
|
|
|
|
|
|
|
matched: list[dict] = []
|
|
|
|
|
for rule in rules:
|
|
|
|
|
condition = rule.get("condition", "")
|
|
|
|
|
try:
|
|
|
|
|
if evaluate_condition(condition, event_obj, context):
|
|
|
|
|
matched.append(rule)
|
|
|
|
|
except UnsafeExpression as exc:
|
|
|
|
|
activity.logger.warning("rule %r unsafe expression — skipping: %s", rule.get("id"), exc)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
activity.logger.warning("rule %r eval error — skipping: %s", rule.get("id"), exc)
|
|
|
|
|
|
|
|
|
|
return matched
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@activity.defn
|
|
|
|
|
async def emit_tasks(payload: dict) -> list[str]:
|
|
|
|
|
"""Emit TaskSpecs to IssueSink and write task_spawn_log rows.
|
|
|
|
|
|
|
|
|
|
Returns list of external task ref IDs.
|
|
|
|
|
|
|
|
|
|
Expected keys in payload:
|
|
|
|
|
task_specs list[dict] — from evaluate_rules matched actions
|
|
|
|
|
activity_id str — UUID of the ActivityDefinition
|
|
|
|
|
triggering_event_id str — event ID or workflow ID for cron
|
|
|
|
|
run_id str — UUID of the ActivityRun
|
|
|
|
|
"""
|
|
|
|
|
from activity_core.rules.models import TaskSpec
|
|
|
|
|
|
|
|
|
|
task_specs_raw = payload.get("task_specs", [])
|
|
|
|
|
activity_id = payload.get("activity_id", "")
|
|
|
|
|
triggering_event_id = payload.get("triggering_event_id", "")
|
|
|
|
|
|
|
|
|
|
sink = get_issue_sink()
|
|
|
|
|
Session = _get_session_factory()
|
|
|
|
|
|
|
|
|
|
refs: list[str] = []
|
|
|
|
|
async with Session() as session:
|
|
|
|
|
async with session.begin():
|
|
|
|
|
for spec_dict in task_specs_raw:
|
|
|
|
|
spec = TaskSpec(
|
|
|
|
|
title=spec_dict.get("title", ""),
|
|
|
|
|
description=spec_dict.get("description", ""),
|
|
|
|
|
target_repo=spec_dict.get("target_repo"),
|
|
|
|
|
priority=spec_dict.get("priority", "medium"),
|
|
|
|
|
labels=spec_dict.get("labels", []),
|
|
|
|
|
due_in_days=spec_dict.get("due_in_days"),
|
|
|
|
|
source_type=spec_dict.get("source_type", "rule"),
|
|
|
|
|
source_id=spec_dict.get("source_id", ""),
|
|
|
|
|
triggering_event_id=triggering_event_id,
|
|
|
|
|
activity_definition_id=activity_id,
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
ref = sink.emit(spec)
|
|
|
|
|
refs.append(ref.external_id)
|
|
|
|
|
|
|
|
|
|
log_row = TaskSpawnLog(
|
|
|
|
|
activity_def_id=uuid.UUID(activity_id),
|
|
|
|
|
source_type=spec.source_type,
|
|
|
|
|
source_id=spec.source_id,
|
|
|
|
|
source_version="1",
|
|
|
|
|
triggering_event_id=triggering_event_id,
|
|
|
|
|
task_ref=ref.external_id,
|
|
|
|
|
condition_matched=spec_dict.get("condition"),
|
|
|
|
|
)
|
|
|
|
|
session.add(log_row)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
activity.logger.warning("emit_tasks: sink.emit failed — %s", exc)
|
|
|
|
|
|
|
|
|
|
return refs
|