Complete release dispatch groundwork and mark remaining work blocked
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e429-436c-73f1-9144-bac4336c06e8
This commit is contained in:
parent
9ebe56bb4f
commit
7b55261bd7
9 changed files with 385 additions and 21 deletions
|
|
@ -16,7 +16,7 @@ COPY .forgejo/workflows/image.yaml ./.forgejo/workflows/image.yaml
|
|||
COPY schemas/ ./schemas/
|
||||
COPY k8s/ ./k8s/
|
||||
COPY scripts/render_gitops.py scripts/check_gitops_promotion.py ./scripts/
|
||||
RUN uv sync --frozen --extra dev && uv run --no-sync python scripts/render_gitops.py --check --verify-frontend --verify-platform && uv run --no-sync pytest -q -p no:cacheprovider tests/test_frontend_patterns.py tests/test_instruction_evaluation.py tests/test_admin_sync_api.py tests/test_gitops_release.py tests/test_release_broker.py tests/test_release_transport.py
|
||||
RUN uv sync --frozen --extra dev && uv run --no-sync python scripts/render_gitops.py --check --verify-frontend --verify-platform && uv run --no-sync pytest -q -p no:cacheprovider tests/test_frontend_patterns.py tests/test_instruction_evaluation.py tests/test_admin_sync_api.py tests/test_gitops_release.py tests/test_release_broker.py tests/test_release_transport.py tests/test_release_dispatch.py
|
||||
|
||||
# Stage 2 — runtime image
|
||||
FROM python:3.12-slim@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea AS runtime
|
||||
|
|
|
|||
|
|
@ -35,15 +35,15 @@
|
|||
| workplan | ACTIVITY-WP-0029 | finished | — | workplans/ACTIVITY-WP-0029-hub-port-alignment.md |
|
||||
| workplan | ACTIVITY-WP-0030 | finished | — | workplans/ACTIVITY-WP-0030-daily-sbom-catchup.md |
|
||||
| workplan | ACTIVITY-WP-0031 | finished | — | workplans/ACTIVITY-WP-0031-production-execution-reliability-cleanup.md |
|
||||
| workplan | ACTIVITY-WP-0032 | active | — | workplans/ACTIVITY-WP-0032-glas-profile-execution-contract.md |
|
||||
| workplan | ACTIVITY-WP-0032 | blocked | — | workplans/ACTIVITY-WP-0032-glas-profile-execution-contract.md |
|
||||
| workplan | ACTIVITY-WP-0033 | finished | — | workplans/ACTIVITY-WP-0033-sbom-catchup-retry-boundary.md |
|
||||
| workplan | ACTIVITY-WP-0034 | finished | — | workplans/ACTIVITY-WP-0034-sbom-controlled-source-reference.md |
|
||||
| workplan | ACTIVITY-WP-0035 | finished | — | workplans/ACTIVITY-WP-0035-intent-boundary-guardrails.md |
|
||||
| workplan | ACTIVITY-WP-0036 | finished | — | workplans/ACTIVITY-WP-0036-queue-identity-and-lease-integrity.md |
|
||||
| workplan | ACTIVITY-WP-0038 | finished | — | workplans/ACTIVITY-WP-0038-repository-grant-close-reconciliation.md |
|
||||
| workplan | ACTIVITY-WP-0039 | finished | — | workplans/ACTIVITY-WP-0039-multi-worker-identity-and-token-custody.md |
|
||||
| workplan | ACTIVITY-WP-0040 | active | — | workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md |
|
||||
| workplan | ACTIVITY-WP-0041 | active | — | workplans/ACTIVITY-WP-0041-gitops-adoption.md |
|
||||
| workplan | ACTIVITY-WP-0040 | blocked | — | workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md |
|
||||
| workplan | ACTIVITY-WP-0041 | blocked | — | workplans/ACTIVITY-WP-0041-gitops-adoption.md |
|
||||
| workplan | ACTIVITY-WP-ADHOC-2026-06-01 | finished | — | workplans/ADHOC-2026-06-01.md |
|
||||
| workplan | ACTIVITY-WP-ADHOC-2026-08-20 | finished | — | workplans/ADHOC-2026-08-20.md |
|
||||
| workplan | ACTIVITY-WP-ADHOC-2026-08-23 | finished | — | workplans/ADHOC-2026-08-23.md |
|
||||
|
|
@ -235,11 +235,11 @@
|
|||
| task | ACTIVITY-WP-0039-T03 | done | — | workplans/ACTIVITY-WP-0039-multi-worker-identity-and-token-custody.md |
|
||||
| task | ACTIVITY-WP-0039-T04 | done | — | workplans/ACTIVITY-WP-0039-multi-worker-identity-and-token-custody.md |
|
||||
| task | ACTIVITY-WP-0040-T01 | done | — | workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md |
|
||||
| task | ACTIVITY-WP-0040-T02 | progress | — | workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md |
|
||||
| task | ACTIVITY-WP-0040-T02 | wait | — | workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md |
|
||||
| task | ACTIVITY-WP-0040-T03 | done | — | workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md |
|
||||
| task | ACTIVITY-WP-0041-T01 | done | — | workplans/ACTIVITY-WP-0041-gitops-adoption.md |
|
||||
| task | ACTIVITY-WP-0041-T02 | progress | — | workplans/ACTIVITY-WP-0041-gitops-adoption.md |
|
||||
| task | ACTIVITY-WP-0041-T03 | progress | — | workplans/ACTIVITY-WP-0041-gitops-adoption.md |
|
||||
| task | ACTIVITY-WP-0041-T02 | wait | — | workplans/ACTIVITY-WP-0041-gitops-adoption.md |
|
||||
| task | ACTIVITY-WP-0041-T03 | wait | — | workplans/ACTIVITY-WP-0041-gitops-adoption.md |
|
||||
| task | ACTIVITY-WP-ADHOC-2026-06-01-T01 | done | — | workplans/ADHOC-2026-06-01.md |
|
||||
| task | ACTIVITY-WP-ADHOC-2026-06-01-T02 | done | — | workplans/ADHOC-2026-06-01.md |
|
||||
| task | ACTIVITY-WP-ADHOC-2026-06-01-T03 | done | — | workplans/ADHOC-2026-06-01.md |
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Image-only release broker core
|
||||
|
||||
ACTIVITY-WP-0041-T03 owns this implementation. **Not activated in production.**
|
||||
The new modules are not wired to an API, worker, schedule or credential source.
|
||||
The modules are not wired to a production API, worker, schedule or credential source.
|
||||
Construction requires `admitted=True` from trusted deployment configuration; that
|
||||
switch is a local guard, not proof that an identity has actually been admitted.
|
||||
The current deployed revision is unchanged, so this code does not restart soak.
|
||||
|
|
@ -62,8 +62,8 @@ production credentials and live Git/ArgoCD rollback are **not** proven by these
|
|||
custody/rotation. The observer must measure continuous health; it must not
|
||||
manufacture a 24-hour interval from two snapshots. Preserve signing public
|
||||
keys for audit and protect the ledger from producer writes.
|
||||
4. Connect the durable dispatcher through activity-core/Temporal and sanitized
|
||||
evidence sinks. Prove authenticated transport failure, concurrency with other
|
||||
4. Deploy/register the implemented Temporal dispatcher and supply its bounded,
|
||||
idempotent sanitized evidence sink. Prove authenticated transport failure, concurrency with other
|
||||
publishers, restart, denial and rollback in an isolated deployment environment.
|
||||
Then finish the production observation gate and enable the bounded scope.
|
||||
|
||||
|
|
@ -103,6 +103,40 @@ snapshots cannot establish the interval. New observers start from their first ac
|
|||
sample; they do not backfill the earlier deployment timestamp.
|
||||
|
||||
Neither module is connected to a production schedule, endpoint or credential source.
|
||||
Trusted signer custody, real invariant probes, Temporal dispatch and isolated
|
||||
Trusted signer custody, real invariant probes, production Temporal registration and isolated
|
||||
Kubernetes authorization/rollback tests remain required before activation. Production
|
||||
revision and the deployment observation clock are unchanged by this source-only work.
|
||||
|
||||
## Temporal dispatch and durable audit delivery
|
||||
|
||||
`release_dispatch.ReleaseDispatchWorkflow` resumes an already-admitted release ID.
|
||||
`ReleaseActivities` binds its broker, backend factory and audit sink at trusted worker
|
||||
construction; Temporal inputs cannot supply manifests, keys, transport configuration
|
||||
or an admission flag. `release_worker` constructs a separate
|
||||
`activity-core-release-tq` worker, which the admitted deployment must explicitly run.
|
||||
It does not modify the ordinary orchestrator worker or start a workflow/schedule.
|
||||
Use a stable workflow ID such as `activity-core-release:<ledger release ID>` when
|
||||
starting it. Broker serialization remains authoritative even with duplicate dispatch.
|
||||
|
||||
Adapter calls run outside the workflow and resume from the durable ledger after
|
||||
activity retry or restart. Failed rollback keeps the release slot and is retried
|
||||
with durable timers; continue-as-new bounds workflow history. Temporal receives
|
||||
sanitized errors rather than raw transport exceptions. Disabling the broker's
|
||||
trusted admission setting prevents subsequent activity calls; operational revocation
|
||||
still requires the admitted worker/credential lifecycle, not a workflow input.
|
||||
|
||||
The transition ledger is also the audit outbox. Acknowledgements are persisted only
|
||||
after the configured sink returns. Delivery is at least once with stable event IDs;
|
||||
the sink must durably upsert those IDs and use bounded I/O. It receives only release
|
||||
ID, phase, time, candidate/rollback commits and the fixed application name. Sink
|
||||
credentials, signed envelopes and transport output never enter these events.
|
||||
Nonterminal sink failure retains pending events and permits recovery to proceed;
|
||||
terminal workflow completion waits for acknowledged audit delivery. One ledger is
|
||||
bound to one logical audit sink; changing sinks requires an explicit replay/migration
|
||||
of delivery acknowledgements. The sink can fan out to approved evidence destinations.
|
||||
|
||||
Tests exercise the real ledger through the activities and validate Temporal sandbox
|
||||
construction, with in-memory transports/sinks and orchestration stubs. This does not
|
||||
prove a deployed Temporal server, real Kubernetes authorization or production sinks.
|
||||
Continuous observer scheduling, actual sink adapters, admission credentials and
|
||||
authenticated deployment proof remain activation prerequisites in T03.
|
||||
|
|
|
|||
135
src/activity_core/release_dispatch.py
Normal file
135
src/activity_core/release_dispatch.py
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
"""Explicitly registered release dispatcher; no production credentials or admission."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
from datetime import timedelta
|
||||
|
||||
from temporalio import activity, workflow
|
||||
from temporalio.common import RetryPolicy
|
||||
from temporalio.exceptions import ActivityError, ApplicationError
|
||||
|
||||
with workflow.unsafe.imports_passed_through():
|
||||
from activity_core.release_broker import TERMINAL, Broker
|
||||
|
||||
|
||||
class ReleaseActivities:
|
||||
"""Trusted worker construction binds the ledger, adapter factory and audit sink.
|
||||
|
||||
The factory receives a ledger plan, never workflow-supplied configuration.
|
||||
The sink must durably upsert by event_id and return only after acknowledgement.
|
||||
This is at-least-once delivery: a lost acknowledgement can repeat an event.
|
||||
"""
|
||||
|
||||
def __init__(self, broker: Broker, backend_factory, sink):
|
||||
if not broker.admitted:
|
||||
raise ValueError("broker identity not admitted")
|
||||
self.broker, self.backend_factory, self.sink = broker, backend_factory, sink
|
||||
with broker.connect() as db:
|
||||
db.execute("CREATE TABLE IF NOT EXISTS release_audit_delivered "
|
||||
"(sequence INTEGER PRIMARY KEY)")
|
||||
|
||||
def _plan(self, release_id):
|
||||
import json
|
||||
|
||||
if not self.broker.admitted:
|
||||
raise ApplicationError("release identity disabled", non_retryable=True)
|
||||
if not isinstance(release_id, str) or not re.fullmatch(r"[0-9a-f]{64}", release_id):
|
||||
raise ApplicationError("invalid release id", non_retryable=True)
|
||||
with self.broker.connect() as db:
|
||||
row = db.execute("SELECT plan FROM releases WHERE id=?", (release_id,)).fetchone()
|
||||
if row is None:
|
||||
raise ApplicationError("unknown admitted release", non_retryable=True)
|
||||
return json.loads(row[0])
|
||||
|
||||
def _advance(self, release_id, plan):
|
||||
return self.broker.advance(release_id, self.backend_factory(plan))
|
||||
|
||||
def _deliver(self, release_id, plan):
|
||||
with self.broker.connect() as db:
|
||||
rows = db.execute(
|
||||
"SELECT sequence, phase, observed_at FROM transitions "
|
||||
"WHERE release_id=? AND sequence NOT IN "
|
||||
"(SELECT sequence FROM release_audit_delivered) "
|
||||
"ORDER BY sequence LIMIT 100", (release_id,),
|
||||
).fetchall()
|
||||
for sequence, phase, observed_at in rows:
|
||||
# Only normalized facts leave the ledger. No envelopes or adapter errors.
|
||||
self.sink({
|
||||
"event_id": f"release:{release_id}:{sequence}",
|
||||
"event_type": "release_transition", "release_id": release_id,
|
||||
"application": "activity-core", "phase": phase,
|
||||
"observed_at": observed_at, "candidate": plan["candidate"],
|
||||
"rollback": plan["rollback"],
|
||||
})
|
||||
with self.broker.connect() as db:
|
||||
db.execute("INSERT OR IGNORE INTO release_audit_delivered VALUES (?)",
|
||||
(sequence,))
|
||||
return len(rows)
|
||||
|
||||
@activity.defn(name="advance_admitted_release")
|
||||
async def advance(self, release_id: str) -> str:
|
||||
plan = self._plan(release_id)
|
||||
try:
|
||||
return await asyncio.to_thread(self._advance, release_id, plan)
|
||||
except Exception:
|
||||
# Do not put transport exception text or credentials in Temporal history.
|
||||
raise ApplicationError("release step unavailable", type="ReleaseStepUnavailable") from None
|
||||
|
||||
@activity.defn(name="deliver_release_audit")
|
||||
async def deliver(self, release_id: str) -> int:
|
||||
plan = self._plan(release_id)
|
||||
try:
|
||||
return await asyncio.to_thread(self._deliver, release_id, plan)
|
||||
except Exception:
|
||||
raise ApplicationError("release audit unavailable", type="ReleaseAuditUnavailable") from None
|
||||
|
||||
|
||||
@workflow.defn
|
||||
class ReleaseDispatchWorkflow:
|
||||
"""Resume one admitted ledger entry; receipt admission stays outside Temporal."""
|
||||
|
||||
@workflow.run
|
||||
async def run(self, release_id: str) -> str:
|
||||
for _ in range(100):
|
||||
phase = await workflow.execute_activity(
|
||||
"advance_admitted_release", release_id,
|
||||
start_to_close_timeout=timedelta(minutes=10),
|
||||
retry_policy=RetryPolicy(maximum_interval=timedelta(seconds=30)),
|
||||
)
|
||||
try:
|
||||
await workflow.execute_activity(
|
||||
"deliver_release_audit", release_id,
|
||||
start_to_close_timeout=timedelta(minutes=1),
|
||||
retry_policy=RetryPolicy(
|
||||
maximum_interval=timedelta(seconds=30),
|
||||
maximum_attempts=0 if phase in TERMINAL else 1,
|
||||
),
|
||||
)
|
||||
except ActivityError:
|
||||
if phase in TERMINAL:
|
||||
raise
|
||||
# A sink outage must not prevent rollback. Committed transitions
|
||||
# stay pending and terminal completion waits for audit delivery.
|
||||
workflow.logger.warning("Release audit pending; ledger retains transitions")
|
||||
if phase in TERMINAL:
|
||||
return phase
|
||||
if phase == "rollback_synced":
|
||||
await workflow.sleep(timedelta(seconds=30))
|
||||
workflow.continue_as_new(release_id)
|
||||
|
||||
|
||||
def release_worker(client, activities: ReleaseActivities):
|
||||
"""Dedicated queue: never add privileged release activities to orchestrator-tq.
|
||||
|
||||
Call only from an admitted deployment with durable shared ledger storage,
|
||||
immutable transport configuration and a bounded, idempotent audit sink.
|
||||
This helper starts no worker, workflow or schedule by itself.
|
||||
"""
|
||||
from temporalio.worker import Worker
|
||||
|
||||
if not activities.broker.admitted:
|
||||
raise ValueError("broker identity not admitted")
|
||||
return Worker(client, task_queue="activity-core-release-tq",
|
||||
workflows=[ReleaseDispatchWorkflow],
|
||||
activities=[activities.advance, activities.deliver])
|
||||
|
|
@ -191,11 +191,18 @@ def test_worker_mounts_ops_inventory_configmap() -> None:
|
|||
pod_spec = deployment["spec"]["template"]["spec"]
|
||||
container = pod_spec["containers"][0]
|
||||
|
||||
mounts = {mount["name"]: mount for mount in container["volumeMounts"]}
|
||||
# One ConfigMap backs both the inventory directory and the pinned retention
|
||||
# script. Volume names are not unique within volumeMounts; paths are.
|
||||
mounts = {mount["mountPath"]: mount for mount in container["volumeMounts"]}
|
||||
volumes = {volume["name"]: volume for volume in pod_spec["volumes"]}
|
||||
|
||||
assert mounts["ops-service-inventory"]["mountPath"] == "/etc/activity-core/ops"
|
||||
assert mounts["ops-service-inventory"]["readOnly"] is True
|
||||
inventory = mounts["/etc/activity-core/ops"]
|
||||
assert inventory["name"] == "ops-service-inventory"
|
||||
assert inventory["readOnly"] is True
|
||||
script = mounts["/opt/railiance-platform/scripts/forgejo_package_prune.py"]
|
||||
assert script["name"] == "ops-service-inventory"
|
||||
assert script["subPath"] == "forgejo_package_prune.py"
|
||||
assert script["readOnly"] is True
|
||||
assert volumes["ops-service-inventory"]["configMap"]["name"] == (
|
||||
"actcore-ops-service-inventory"
|
||||
)
|
||||
|
|
|
|||
140
tests/test_release_dispatch.py
Normal file
140
tests/test_release_dispatch.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
"""Durable dispatch and audit recovery without production authority or transports."""
|
||||
import pytest
|
||||
from temporalio.exceptions import ActivityError, ApplicationError
|
||||
from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner
|
||||
from temporalio.workflow import _Definition
|
||||
|
||||
from activity_core.release_broker import Broker, TERMINAL
|
||||
from activity_core.release_dispatch import ReleaseActivities, ReleaseDispatchWorkflow
|
||||
from tests.test_release_broker import NOW, FakeBackend, admit, bundle # noqa: F401
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dispatch(bundle, monkeypatch):
|
||||
broker, *_ = bundle
|
||||
rid = admit(bundle)
|
||||
backend = FakeBackend()
|
||||
advance = Broker.advance
|
||||
monkeypatch.setattr(Broker, 'advance', lambda self, rid, backend: advance(self, rid, backend, NOW))
|
||||
events = {}
|
||||
sink = lambda event: events.update({event['event_id']: event})
|
||||
activities = ReleaseActivities(broker, lambda plan: backend, sink)
|
||||
return activities, rid, backend, events, sink
|
||||
|
||||
|
||||
async def test_restart_delivers_all_committed_transitions_and_deduplicates(dispatch):
|
||||
activities, rid, backend, events, sink = dispatch
|
||||
assert await activities.advance(rid) == 'publish_pending'
|
||||
assert await activities.advance(rid) == 'published'
|
||||
# Restart before audit delivery. The ledger contains both undelivered steps.
|
||||
restarted = ReleaseActivities(
|
||||
Broker(activities.broker.database, activities.broker.receipts, admitted=True),
|
||||
lambda plan: backend, sink,
|
||||
)
|
||||
assert await restarted.deliver(rid) == 3
|
||||
assert await restarted.deliver(rid) == 0
|
||||
while await restarted.advance(rid) not in TERMINAL:
|
||||
pass
|
||||
assert await restarted.deliver(rid) == 2
|
||||
assert [e['phase'] for e in events.values()] == [
|
||||
'planned', 'publish_pending', 'published', 'synced', 'complete',
|
||||
]
|
||||
assert all(set(e) == {'event_id', 'event_type', 'release_id', 'application',
|
||||
'phase', 'observed_at', 'candidate', 'rollback'}
|
||||
for e in events.values())
|
||||
|
||||
|
||||
async def test_lost_audit_ack_replays_same_event_without_losing_it(dispatch):
|
||||
activities, rid, _, events, sink = dispatch
|
||||
def lost_ack(event):
|
||||
sink(event)
|
||||
raise RuntimeError('secret sink token must not reach Temporal')
|
||||
activities.sink = lost_ack
|
||||
with pytest.raises(ApplicationError) as error:
|
||||
await activities.deliver(rid)
|
||||
assert str(error.value) == 'ReleaseAuditUnavailable: release audit unavailable'
|
||||
assert error.value.__suppress_context__
|
||||
activities.sink = sink
|
||||
assert await activities.deliver(rid) == 1
|
||||
assert len(events) == 1
|
||||
|
||||
|
||||
async def test_lost_publication_response_resumes_and_rolls_back(dispatch):
|
||||
activities, rid, backend, events, _ = dispatch
|
||||
backend.lose_response = True
|
||||
await activities.advance(rid)
|
||||
with pytest.raises(ApplicationError, match='release step unavailable'):
|
||||
await activities.advance(rid)
|
||||
backend.fail_health = True
|
||||
for _ in range(10):
|
||||
phase = await activities.advance(rid)
|
||||
await activities.deliver(rid)
|
||||
if phase in TERMINAL:
|
||||
break
|
||||
assert phase == 'rolled_back'
|
||||
assert backend.revision == 'b' * 40
|
||||
assert list(events.values())[-1]['phase'] == 'rolled_back'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('rid', ['untrusted', 'f' * 64, {'key': 'caller config'}])
|
||||
async def test_only_existing_admitted_id_accepted(dispatch, rid):
|
||||
activities, _, backend, events, _ = dispatch
|
||||
with pytest.raises(ApplicationError) as error:
|
||||
await activities.advance(rid)
|
||||
assert error.value.non_retryable
|
||||
assert not backend.calls and not events
|
||||
|
||||
|
||||
async def test_revocation_blocks_resume_and_delivery(dispatch):
|
||||
activities, rid, backend, events, _ = dispatch
|
||||
activities.broker.admitted = False
|
||||
for call in (activities.advance, activities.deliver):
|
||||
with pytest.raises(ApplicationError) as error:
|
||||
await call(rid)
|
||||
assert error.value.non_retryable
|
||||
assert not backend.calls and not events
|
||||
|
||||
|
||||
async def test_temporal_sandbox_and_workflow_rollback(dispatch, monkeypatch):
|
||||
# Validate actual Temporal sandbox imports, then exercise orchestration with
|
||||
# real ledger activities. No Temporal server or simulated passage of soak time.
|
||||
SandboxedWorkflowRunner().prepare_workflow(_Definition.must_from_class(ReleaseDispatchWorkflow))
|
||||
activities, rid, backend, events, _ = dispatch
|
||||
backend.fail_health = True
|
||||
async def execute(name, arg, **kwargs):
|
||||
return await (activities.advance(arg) if name == 'advance_admitted_release'
|
||||
else activities.deliver(arg))
|
||||
async def sleep(_):
|
||||
pass
|
||||
monkeypatch.setattr('activity_core.release_dispatch.workflow.execute_activity', execute)
|
||||
monkeypatch.setattr('activity_core.release_dispatch.workflow.sleep', sleep)
|
||||
assert await ReleaseDispatchWorkflow().run(rid) == 'rolled_back'
|
||||
assert list(events.values())[-1]['phase'] == 'rolled_back'
|
||||
|
||||
|
||||
async def test_sink_outage_cannot_prevent_rollback(dispatch, monkeypatch):
|
||||
activities, rid, backend, events, _ = dispatch
|
||||
backend.fail_health = True
|
||||
phases = []
|
||||
async def execute(name, arg, **kwargs):
|
||||
if name == 'advance_admitted_release':
|
||||
phase = await activities.advance(arg)
|
||||
phases.append(phase)
|
||||
return phase
|
||||
if phases[-1] not in TERMINAL:
|
||||
assert kwargs['retry_policy'].maximum_attempts == 1
|
||||
raise ActivityError('audit unavailable', scheduled_event_id=1,
|
||||
started_event_id=2, identity='fixture',
|
||||
activity_type=name, activity_id='fixture', retry_state=None)
|
||||
assert kwargs['retry_policy'].maximum_attempts == 0
|
||||
return await activities.deliver(arg)
|
||||
async def sleep(_):
|
||||
pass
|
||||
monkeypatch.setattr('activity_core.release_dispatch.workflow.execute_activity', execute)
|
||||
monkeypatch.setattr('activity_core.release_dispatch.workflow.sleep', sleep)
|
||||
# A plain logger avoids requiring a live workflow runtime for this unit test.
|
||||
import logging
|
||||
monkeypatch.setattr('activity_core.release_dispatch.workflow.logger', logging.getLogger(__name__))
|
||||
assert await ReleaseDispatchWorkflow().run(rid) == 'rolled_back'
|
||||
assert backend.revision == 'b' * 40
|
||||
assert len(events) == len(phases) + 1 # Includes the admission transition.
|
||||
|
|
@ -4,13 +4,13 @@ type: workplan
|
|||
title: "Adopt the Glas profile-driven execution contract"
|
||||
domain: infotech
|
||||
repo: activity-core
|
||||
status: active
|
||||
status: blocked
|
||||
flavor: implementation
|
||||
owner: claude
|
||||
topic_slug: activity-core
|
||||
priority: medium
|
||||
created: "2026-08-21"
|
||||
updated: "2026-09-04"
|
||||
updated: "2026-09-27"
|
||||
related:
|
||||
- ACT-ADR-006
|
||||
- ACTIVITY-WP-0026
|
||||
|
|
@ -299,3 +299,15 @@ was requested or copied, and the disabled pilot remains untouched.
|
|||
- [x] `approach_hint` cannot override or substitute for a profile ref, proven by test
|
||||
- [x] Normalized Glas evidence is visible in production status
|
||||
- [ ] One definition proven on railiance01 end to end
|
||||
|
||||
## Loose-end review — 2026-09-27
|
||||
|
||||
T05 remains blocked outside this repository. Current owner evidence supersedes
|
||||
the September 4 description: sand-boxer has implemented owner-mediated execution
|
||||
and protected runtime placement, but GLAS-WP-0012 remains blocked. The original
|
||||
1.0.0 pilot profile is still explicitly blocked; 1.1.1 is unverified and needs
|
||||
its updated runtime and combined production proof. Native credential admission,
|
||||
provider execution and profile readiness remain owner gates. Do not rerun the
|
||||
old pilot or silently change its profile. Resume T05 after Glas supplies an
|
||||
admitted profile and matching railiance01 runtime/credential evidence. Workplan
|
||||
state is now blocked; the four completed implementation tasks remain done.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ type: workplan
|
|||
title: "Deterministic frontend-patterns feedback collection and reports"
|
||||
domain: infotech
|
||||
repo: activity-core
|
||||
status: active
|
||||
status: blocked
|
||||
owner: codex
|
||||
topic_slug: activity-core
|
||||
created: "2026-09-27"
|
||||
|
|
@ -35,7 +35,7 @@ Dockerfile.frontend-patterns overlays only two Python files on the deployed work
|
|||
|
||||
```task
|
||||
id: ACTIVITY-WP-0040-T02
|
||||
status: progress
|
||||
status: wait
|
||||
priority: high
|
||||
state_hub_task_id: "0d0d1d05-9b2d-5eac-b54a-c557b1690afc"
|
||||
```
|
||||
|
|
@ -88,3 +88,13 @@ FEP-WP-0008 retains consumers and improvement execution; ACTIVITY-WP-0041 record
|
|||
the permanent GitOps/adoption/standing-authority work needed to prevent exceptions.
|
||||
Do not close this workplan until cadence evidence is observed or explicitly handed
|
||||
off to another live task. No real-consumer value or autonomous edits are claimed.
|
||||
|
||||
## Loose-end review — 2026-09-27
|
||||
|
||||
T02 is now wait and the workplan blocked on natural cadence evidence. The
|
||||
September 27 activation/sink smoke already completed the deployable repo work;
|
||||
first daily/weekly/monthly executions are due September 28 / September 29 /
|
||||
October 1. A manual rerun cannot satisfy this acceptance condition. Resume T02
|
||||
when the three natural executions have matching run/report/sink receipts. Keep
|
||||
existing schedules enabled and retain FEP-WP-0008 ownership of consumer value
|
||||
and improvement execution. No new task or artificial success evidence was created.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ type: workplan
|
|||
title: "Remove recurring deployment exceptions through governed GitOps adoption"
|
||||
domain: infotech
|
||||
repo: activity-core
|
||||
status: active
|
||||
status: blocked
|
||||
owner: codex
|
||||
topic_slug: activity-core
|
||||
created: "2026-09-27"
|
||||
|
|
@ -44,7 +44,7 @@ server-dry-run the proposed managed set; no unintended spec change or pruning.
|
|||
|
||||
```task
|
||||
id: ACTIVITY-WP-0041-T02
|
||||
status: progress
|
||||
status: wait
|
||||
priority: high
|
||||
state_hub_task_id: "af09865d-8cc9-5571-9c34-20ae679a1e4e"
|
||||
```
|
||||
|
|
@ -67,7 +67,7 @@ one-time governance decision.
|
|||
|
||||
```task
|
||||
id: ACTIVITY-WP-0041-T03
|
||||
status: progress
|
||||
status: wait
|
||||
priority: high
|
||||
state_hub_task_id: "8e2b4ed9-d455-5367-85d9-8e4222ce75f3"
|
||||
```
|
||||
|
|
@ -191,3 +191,29 @@ T03 remains progress for admitted identity/custody, authenticated isolated Kuber
|
|||
verification, real build/review/retention issuers, production invariant probes,
|
||||
Temporal observer/dispatcher registration and evidence sinks. The implemented
|
||||
observer cannot retroactively assert the earlier soak interval. See docs/release-broker.md.
|
||||
|
||||
## Loose-end implementation and blockers — 2026-09-27
|
||||
|
||||
Completed another repo-side portion of T03: `release_dispatch.py` supplies a
|
||||
Temporal workflow and explicitly constructed dedicated worker for resuming an
|
||||
already-admitted ledger release. Requests carry only its ID; source, transport,
|
||||
keys and sinks come from trusted worker configuration. Durable transition audit
|
||||
acknowledgements survive restart, repeat the same event ID after a lost response,
|
||||
and export only normalized facts. Nonterminal audit outages do not prevent
|
||||
rollback; terminal completion waits for audit delivery. Adapter errors are
|
||||
sanitized before reaching Temporal history. The ordinary worker remains unchanged.
|
||||
|
||||
Tests cover restart, lost publication and audit responses, rollback, unknown IDs,
|
||||
identity disablement and Temporal sandbox construction. The image CI test target
|
||||
includes the new suite. This is isolated implementation evidence, not authenticated
|
||||
Kubernetes proof or production activation.
|
||||
|
||||
T02 and T03 are now wait and the workplan blocked. T02 cannot close before the
|
||||
September 28 16:06:22 Berlin observation eligibility and healthy owner evidence
|
||||
(RPF-WP-0048-T02). T03 still requires admitted identity/custody, real independent
|
||||
receipt issuers, bounded production report/schedule probes, authenticated isolated
|
||||
transport/denial/rollback proof, and deployment/registration of the observer,
|
||||
dispatcher and audit sink. Local code cannot supply those trust inputs. The new
|
||||
worker factory neither enables a schedule nor grants an identity; deployment and
|
||||
continuous health observation must be established through the existing owner lane.
|
||||
No new workplan/task, production mutation or authority expansion was introduced.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue