feat(dataflow): preserve publication review outcomes
Module Package Release / publish-packages (push) Successful in 13s

This commit is contained in:
2026-08-21 17:36:19 +02:00
parent 6767905cbb
commit a86220db27
7 changed files with 84 additions and 11 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-dataflow" name = "govoplan-dataflow"
version = "0.1.18" version = "0.1.19"
description = "Governed graphical and SQL data pipelines for GovOPlaN." description = "Governed graphical and SQL data pipelines for GovOPlaN."
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
+1 -1
View File
@@ -1,3 +1,3 @@
from __future__ import annotations from __future__ import annotations
__version__ = "0.1.18" __version__ = "0.1.19"
+17 -3
View File
@@ -56,7 +56,7 @@ from govoplan_dataflow.backend.dsar_provider import (
MODULE_ID = "dataflow" MODULE_ID = "dataflow"
MODULE_NAME = "Dataflow" MODULE_NAME = "Dataflow"
MODULE_VERSION = "0.1.18" MODULE_VERSION = "0.1.19"
READ_SCOPE = "dataflow:pipeline:read" READ_SCOPE = "dataflow:pipeline:read"
WRITE_SCOPE = "dataflow:pipeline:write" WRITE_SCOPE = "dataflow:pipeline:write"
@@ -262,7 +262,12 @@ DOCUMENTATION = (
"Scope determines ownership and Policy inheritance. Templates can be derived but not run; complete " "Scope determines ownership and Policy inheritance. Templates can be derived but not run; complete "
"flows may be previewed, revisioned, automated, and executed when effective Policy allows it. Saving " "flows may be previewed, revisioned, automated, and executed when effective Policy allows it. Saving "
"appends an immutable revision. A scoped copy pins its source revision and content hash. Triggers pin " "appends an immutable revision. A scoped copy pins its source revision and content hash. Triggers pin "
"the revision and authorization grant, then re-evaluate authority for every delivery. Runs create " "the revision and authorization grant, then re-evaluate authority for every delivery. Allow runs is "
"the definition-level admission boundary and does not grant a caller permission. A one-time run uses "
"the configured tenant-local date and time. The missed-run policy either coalesces elapsed interval "
"occurrences into one latest delivery or skips them; it never silently replays every missed occurrence. "
"The concurrency limit bounds active deliveries for that trigger and does not increase tenant worker "
"capacity. Runs create "
"durable command and recovery evidence. Publishing creates a governed Datasource materialization, and " "durable command and recovery evidence. Publishing creates a governed Datasource materialization, and "
"environment promotion changes which immutable revision is eligible for staging or production runs. " "environment promotion changes which immutable revision is eligible for staging or production runs. "
"Recording a reconciliation decision uses optimistic concurrency and appends an immutable revision; " "Recording a reconciliation decision uses optimistic concurrency and appends an immutable revision; "
@@ -285,6 +290,10 @@ DOCUMENTATION = (
"help_contexts": [ "help_contexts": [
"dataflow.field.scope", "dataflow.field.scope",
"dataflow.field.definition-kind", "dataflow.field.definition-kind",
"dataflow.field.allow-runs",
"dataflow.field.trigger-run-at",
"dataflow.field.trigger-missed-runs",
"dataflow.field.trigger-concurrency",
"dataflow.action.save", "dataflow.action.save",
"dataflow.action.derive", "dataflow.action.derive",
"dataflow.action.trigger", "dataflow.action.trigger",
@@ -309,7 +318,11 @@ DOCUMENTATION = (
"environment, progress, cancellation, output, and recovery state. Database-only runs commit atomically. " "environment, progress, cancellation, output, and recovery state. Database-only runs commit atomically. "
"Publication to a governed Datasource uses forward recovery: an unknown provider outcome is reconciled " "Publication to a governed Datasource uses forward recovery: an unknown provider outcome is reconciled "
"before retry so output is not duplicated. Staging and production promotion is explicit and does not " "before retry so output is not duplicated. Staging and production promotion is explicit and does not "
"rewrite a revision. Cancellation is best effort once external work has started; the final evidence " "rewrite a revision. Freezing a published state assigns a durable label to the exact immutable output; "
"it does not copy or detach the data from Datasources retention, hold, and access rules. Artifact-backed "
"outputs return the same stable publication, datasource, and materialization references as inline "
"outputs. Datasource warnings and review-required states remain visible to Workflow instead of being "
"collapsed into success. Cancellation is best effort once external work has started; the final evidence "
"states whether work stopped, completed, failed, or requires operator reconciliation. Scheduled, event, " "states whether work stopped, completed, failed, or requires operator reconciliation. Scheduled, event, "
"and queued execution is partitioned by tenant module entitlement before a run is claimed. Disabling " "and queued execution is partitioned by tenant module entitlement before a run is claimed. Disabling "
"Dataflow stops new admission and leaves accepted runs available for an explicit operator decision." "Dataflow stops new admission and leaves accepted runs available for an explicit operator decision."
@@ -325,6 +338,7 @@ DOCUMENTATION = (
"dataflow.runs", "dataflow.runs",
"dataflow.action.queue-run", "dataflow.action.queue-run",
"dataflow.action.publish", "dataflow.action.publish",
"dataflow.field.freeze-publication",
"dataflow.action.promote-staging", "dataflow.action.promote-staging",
"dataflow.action.promote-production", "dataflow.action.promote-production",
"dataflow.state.recovery-attention", "dataflow.state.recovery-attention",
+20
View File
@@ -1584,6 +1584,26 @@ def _publish_pipeline_result(
run.output_publication_ref = publication.ref run.output_publication_ref = publication.ref
run.output_datasource_ref = publication.datasource.ref run.output_datasource_ref = publication.datasource.ref
run.output_materialization_ref = publication.materialization.ref run.output_materialization_ref = publication.materialization.ref
if publication.status in {"published_with_warnings", "review_required"}:
diagnostics = list(run.diagnostics)
diagnostics.append(
DataflowDiagnostic(
severity="warning",
code=(
"publication.review_required"
if publication.status == "review_required"
else "publication.warning"
),
message=(
"The output materialization requires review before it can "
"become the datasource's current state."
if publication.status == "review_required"
else "The output was published with datasource validation "
"warnings."
),
).model_dump(mode="json")
)
run.diagnostics = diagnostics
def _mark_pipeline_run_failed( def _mark_pipeline_run_failed(
+33 -2
View File
@@ -123,8 +123,9 @@ def runtime_identity() -> RuntimeIdentity:
class FakePublicationProvider: class FakePublicationProvider:
def __init__(self) -> None: def __init__(self, status: str = "published") -> None:
self.requests = [] self.requests = []
self.status = status
def publish_rows(self, _session, _principal, *, request): def publish_rows(self, _session, _principal, *, request):
self.requests.append(request) self.requests.append(request)
@@ -139,7 +140,7 @@ class FakePublicationProvider:
) )
return DatasourcePublicationResult( return DatasourcePublicationResult(
ref="publication:publication-1", ref="publication:publication-1",
status="published", status=self.status,
datasource=descriptor, datasource=descriptor,
materialization=DatasourceMaterialization( materialization=DatasourceMaterialization(
ref="materialization:materialization-1", ref="materialization:materialization-1",
@@ -539,6 +540,36 @@ class DataflowServiceTests(unittest.TestCase):
), ),
) )
def test_publication_review_state_is_exposed_to_workflow_as_a_warning(
self,
) -> None:
pipeline = self._create()
provider = FakePublicationProvider("review_required")
run, _ = start_pipeline_run(
self.session,
tenant_id="tenant-1",
actor_id="user-1",
principal=principal(),
registry=FakeRegistry(provider),
request=DataflowRunRequest(
pipeline_ref=f"pipeline:{pipeline.id}",
revision=1,
idempotency_key="review-publication",
publication=DataflowPublicationTarget(
name="Review output",
source_name="review_output",
),
),
)
self.assertEqual("succeeded", run.status)
self.assertEqual(
"publication.review_required",
run.diagnostics[-1]["code"],
)
self.assertEqual("warning", run.diagnostics[-1]["severity"])
def test_publication_without_datasources_finishes_as_failed_run(self) -> None: def test_publication_without_datasources_finishes_as_failed_run(self) -> None:
pipeline = self._create() pipeline = self._create()
run, _ = start_pipeline_run( run, _ = start_pipeline_run(
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/dataflow-webui", "name": "@govoplan/dataflow-webui",
"version": "0.1.18", "version": "0.1.19",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+11 -3
View File
@@ -1201,6 +1201,10 @@ function DefinitionSettingsDialog({
/> />
<ToggleSwitch <ToggleSwitch
label="Allow runs" label="Allow runs"
interfaceId="dataflow.field.allow-runs"
helpContextId="dataflow.field.allow-runs"
helpModuleId="dataflow"
helpTopicId="dataflow.reference.fields-and-consequences"
checked={draft.allowRun} checked={draft.allowRun}
disabled={!editable} disabled={!editable}
onChange={(value) => onChange({ allowRun: value })} onChange={(value) => onChange({ allowRun: value })}
@@ -1737,7 +1741,7 @@ function DataflowTriggersDialog({
</select> </select>
</FormField> </FormField>
{kind === "once" ? ( {kind === "once" ? (
<FormField label="Run at" documentation={DATAFLOW_FIELDS_DOCUMENTATION}> <FormField label="Run at" interfaceId="dataflow.field.trigger-run-at" helpContextId="dataflow.field.trigger-run-at" helpModuleId="dataflow" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
<input disabled={!editable} type="datetime-local" value={runAt} onChange={(event) => setRunAt(event.target.value)} /> <input disabled={!editable} type="datetime-local" value={runAt} onChange={(event) => setRunAt(event.target.value)} />
</FormField> </FormField>
) : null} ) : null}
@@ -1752,7 +1756,7 @@ function DataflowTriggersDialog({
onChange={(event) => setIntervalMinutes(Math.max(1, Number(event.target.value)))} onChange={(event) => setIntervalMinutes(Math.max(1, Number(event.target.value)))}
/> />
</FormField> </FormField>
<FormField label="Missed runs" documentation={DATAFLOW_FIELDS_DOCUMENTATION}> <FormField label="Missed runs" interfaceId="dataflow.field.trigger-missed-runs" helpContextId="dataflow.field.trigger-missed-runs" helpModuleId="dataflow" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
<select <select
disabled={!editable} disabled={!editable}
value={catchUpPolicy} value={catchUpPolicy}
@@ -1787,7 +1791,7 @@ function DataflowTriggersDialog({
disabled={busy || !editable} disabled={busy || !editable}
onChange={setEnabled} onChange={setEnabled}
/> />
<FormField label="Concurrent runs" documentation={DATAFLOW_FIELDS_DOCUMENTATION}> <FormField label="Concurrent runs" interfaceId="dataflow.field.trigger-concurrency" helpContextId="dataflow.field.trigger-concurrency" helpModuleId="dataflow" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
<input <input
type="number" type="number"
min={1} min={1}
@@ -2155,6 +2159,10 @@ function RunPipelineDialog({
<div className="dataflow-run-freeze"> <div className="dataflow-run-freeze">
<ToggleSwitch <ToggleSwitch
label="Freeze published state" label="Freeze published state"
interfaceId="dataflow.field.freeze-publication"
helpContextId="dataflow.field.freeze-publication"
helpModuleId="dataflow"
helpTopicId="dataflow.execution-and-recovery"
checked={freeze} checked={freeze}
onChange={setFreeze} onChange={setFreeze}
/> />