feat(task-017): implement git publishing dry-run and commit flow

This commit is contained in:
2026-05-22 00:56:53 +03:00
parent d3990b4019
commit 318ae790ef
15 changed files with 1971 additions and 17 deletions
@@ -32,6 +32,7 @@ from src.domain.contracts import (
PlanReviewStatus,
PlanSectionSummary,
PlanSummary,
PublishCommitSummary,
PublishingRules,
PublishingStatus,
ResearchArtifactManifestSummary,
@@ -67,6 +68,7 @@ class BackendRepository:
self.content_reviews = ContentReviewsRepository(self)
self.assets = AssetsRepository(self)
self.research_manifests = ResearchManifestsRepository(self)
self.publish_commits = PublishCommitsRepository(self)
self.evidence_items = EvidenceItemsRepository(self)
self.claims = ClaimsRepository(self)
self.agent_jobs = AgentJobsRepository(self)
@@ -568,6 +570,26 @@ class ArticlesRepository:
return self.get(article_id)
def update_publishing_status(
self,
*,
article_id: UUID,
publishing_status: PublishingStatus,
updated_at: datetime,
) -> ArticleSummary:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
connection.execute(
f"""
UPDATE articles
SET publishing_status = {placeholder}, updated_at = {placeholder}
WHERE id = {placeholder}
""",
(publishing_status.value, _datetime_value(updated_at), str(article_id)),
)
return self.get(article_id)
def create_workflow_event(
self,
*,
@@ -2049,6 +2071,150 @@ class ResearchManifestsRepository:
return _research_manifest_from_row(row)
class PublishCommitsRepository:
def __init__(self, repository: BackendRepository) -> None:
self._repository = repository
def create(
self,
*,
article_id: UUID,
target_site_id: UUID,
repository_url: str,
branch: str,
commit_sha: str | None,
content_bundle_manifest: JsonObject,
status: PublishingStatus,
deployment_status: str | None,
created_at: datetime,
) -> PublishCommitSummary:
publish_commit_id = uuid4()
placeholder = self._repository.placeholder()
json_cast = self._repository.json_cast()
with self._repository.connection() as connection:
connection.execute(
f"""
INSERT INTO publish_commits (
id,
article_id,
target_site_id,
repository_url,
branch,
commit_sha,
content_bundle_manifest,
status,
deployment_status,
created_at
)
VALUES (
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder},
{placeholder}{json_cast},
{placeholder},
{placeholder},
{placeholder}
)
""",
(
str(publish_commit_id),
str(article_id),
str(target_site_id),
repository_url,
branch,
commit_sha,
_json_value(content_bundle_manifest),
status.value,
deployment_status,
_datetime_value(created_at),
),
)
return self.get(publish_commit_id)
def get(self, publish_commit_id: UUID) -> PublishCommitSummary:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
row = connection.execute(
f"""
SELECT {self._select_columns()}
FROM publish_commits
WHERE id = {placeholder}
""",
(str(publish_commit_id),),
).fetchone()
if row is None:
raise LookupError(f"Publish commit not found: {publish_commit_id}")
return _publish_commit_from_row(row)
def list_for_article(self, article_id: UUID) -> list[PublishCommitSummary]:
placeholder = self._repository.placeholder()
with self._repository.connection() as connection:
rows = connection.execute(
f"""
SELECT {self._select_columns()}
FROM publish_commits
WHERE article_id = {placeholder}
ORDER BY created_at DESC, id DESC
""",
(str(article_id),),
).fetchall()
return [_publish_commit_from_row(row) for row in rows]
def latest_for_article(self, article_id: UUID) -> PublishCommitSummary | None:
commits = self.list_for_article(article_id)
if not commits:
return None
return commits[0]
def latest_for_article_with_statuses(
self,
article_id: UUID,
*,
statuses: set[PublishingStatus],
) -> PublishCommitSummary | None:
status_values = [status.value for status in statuses]
if not status_values:
return None
placeholder = self._repository.placeholder()
status_placeholders = ", ".join([placeholder] * len(status_values))
with self._repository.connection() as connection:
row = connection.execute(
f"""
SELECT {self._select_columns()}
FROM publish_commits
WHERE article_id = {placeholder}
AND status IN ({status_placeholders})
ORDER BY created_at DESC, id DESC
LIMIT 1
""",
(str(article_id), *status_values),
).fetchone()
if row is None:
return None
return _publish_commit_from_row(row)
def _select_columns(self) -> str:
return """
id,
article_id,
target_site_id,
repository_url,
branch,
commit_sha,
content_bundle_manifest,
status,
deployment_status,
created_at
"""
class EvidenceItemsRepository:
def __init__(self, repository: BackendRepository) -> None:
self._repository = repository
@@ -3066,6 +3232,21 @@ def _research_manifest_from_row(row: Any) -> ResearchArtifactManifestSummary:
)
def _publish_commit_from_row(row: Any) -> PublishCommitSummary:
return PublishCommitSummary(
id=_row_value(row, "id"),
article_id=_row_value(row, "article_id"),
target_site_id=_row_value(row, "target_site_id"),
repository_url=_row_value(row, "repository_url"),
branch=_row_value(row, "branch"),
commit_sha=_row_value(row, "commit_sha"),
content_bundle_manifest=_json_from_row(row, "content_bundle_manifest"),
status=_row_value(row, "status"),
deployment_status=_row_value(row, "deployment_status"),
created_at=_row_value(row, "created_at"),
)
def _evidence_from_row(row: Any) -> EvidenceSummary:
return EvidenceSummary(
id=_row_value(row, "id"),