Complete release dispatch groundwork and mark remaining work blocked
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 1m32s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e429-436c-73f1-9144-bac4336c06e8
This commit is contained in:
tegwick 2026-09-27 20:51:56 +02:00
parent 9ebe56bb4f
commit 7b55261bd7
9 changed files with 385 additions and 21 deletions

View file

@ -16,7 +16,7 @@ COPY .forgejo/workflows/image.yaml ./.forgejo/workflows/image.yaml
COPY schemas/ ./schemas/ COPY schemas/ ./schemas/
COPY k8s/ ./k8s/ COPY k8s/ ./k8s/
COPY scripts/render_gitops.py scripts/check_gitops_promotion.py ./scripts/ 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 # Stage 2 — runtime image
FROM python:3.12-slim@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea AS runtime FROM python:3.12-slim@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea AS runtime

View file

@ -35,15 +35,15 @@
| workplan | ACTIVITY-WP-0029 | finished | — | workplans/ACTIVITY-WP-0029-hub-port-alignment.md | | 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-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-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-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-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-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-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-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-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-0040 | blocked | — | workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md |
| workplan | ACTIVITY-WP-0041 | active | — | workplans/ACTIVITY-WP-0041-gitops-adoption.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-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-20 | finished | — | workplans/ADHOC-2026-08-20.md |
| workplan | ACTIVITY-WP-ADHOC-2026-08-23 | finished | — | workplans/ADHOC-2026-08-23.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-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-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-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-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-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-T02 | wait | — | workplans/ACTIVITY-WP-0041-gitops-adoption.md |
| task | ACTIVITY-WP-0041-T03 | progress | — | 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-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-T02 | done | — | workplans/ADHOC-2026-06-01.md |
| task | ACTIVITY-WP-ADHOC-2026-06-01-T03 | done | — | workplans/ADHOC-2026-06-01.md | | task | ACTIVITY-WP-ADHOC-2026-06-01-T03 | done | — | workplans/ADHOC-2026-06-01.md |

View file

@ -1,7 +1,7 @@
# Image-only release broker core # Image-only release broker core
ACTIVITY-WP-0041-T03 owns this implementation. **Not activated in production.** 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 Construction requires `admitted=True` from trusted deployment configuration; that
switch is a local guard, not proof that an identity has actually been admitted. 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. 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 custody/rotation. The observer must measure continuous health; it must not
manufacture a 24-hour interval from two snapshots. Preserve signing public manufacture a 24-hour interval from two snapshots. Preserve signing public
keys for audit and protect the ledger from producer writes. keys for audit and protect the ledger from producer writes.
4. Connect the durable dispatcher through activity-core/Temporal and sanitized 4. Deploy/register the implemented Temporal dispatcher and supply its bounded,
evidence sinks. Prove authenticated transport failure, concurrency with other idempotent sanitized evidence sink. Prove authenticated transport failure, concurrency with other
publishers, restart, denial and rollback in an isolated deployment environment. publishers, restart, denial and rollback in an isolated deployment environment.
Then finish the production observation gate and enable the bounded scope. 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. sample; they do not backfill the earlier deployment timestamp.
Neither module is connected to a production schedule, endpoint or credential source. 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 Kubernetes authorization/rollback tests remain required before activation. Production
revision and the deployment observation clock are unchanged by this source-only work. 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.

View 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])

View file

@ -191,11 +191,18 @@ def test_worker_mounts_ops_inventory_configmap() -> None:
pod_spec = deployment["spec"]["template"]["spec"] pod_spec = deployment["spec"]["template"]["spec"]
container = pod_spec["containers"][0] 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"]} volumes = {volume["name"]: volume for volume in pod_spec["volumes"]}
assert mounts["ops-service-inventory"]["mountPath"] == "/etc/activity-core/ops" inventory = mounts["/etc/activity-core/ops"]
assert mounts["ops-service-inventory"]["readOnly"] is True 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"] == ( assert volumes["ops-service-inventory"]["configMap"]["name"] == (
"actcore-ops-service-inventory" "actcore-ops-service-inventory"
) )

View 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.

View file

@ -4,13 +4,13 @@ type: workplan
title: "Adopt the Glas profile-driven execution contract" title: "Adopt the Glas profile-driven execution contract"
domain: infotech domain: infotech
repo: activity-core repo: activity-core
status: active status: blocked
flavor: implementation flavor: implementation
owner: claude owner: claude
topic_slug: activity-core topic_slug: activity-core
priority: medium priority: medium
created: "2026-08-21" created: "2026-08-21"
updated: "2026-09-04" updated: "2026-09-27"
related: related:
- ACT-ADR-006 - ACT-ADR-006
- ACTIVITY-WP-0026 - 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] `approach_hint` cannot override or substitute for a profile ref, proven by test
- [x] Normalized Glas evidence is visible in production status - [x] Normalized Glas evidence is visible in production status
- [ ] One definition proven on railiance01 end to end - [ ] 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.

View file

@ -4,7 +4,7 @@ type: workplan
title: "Deterministic frontend-patterns feedback collection and reports" title: "Deterministic frontend-patterns feedback collection and reports"
domain: infotech domain: infotech
repo: activity-core repo: activity-core
status: active status: blocked
owner: codex owner: codex
topic_slug: activity-core topic_slug: activity-core
created: "2026-09-27" created: "2026-09-27"
@ -35,7 +35,7 @@ Dockerfile.frontend-patterns overlays only two Python files on the deployed work
```task ```task
id: ACTIVITY-WP-0040-T02 id: ACTIVITY-WP-0040-T02
status: progress status: wait
priority: high priority: high
state_hub_task_id: "0d0d1d05-9b2d-5eac-b54a-c557b1690afc" 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. the permanent GitOps/adoption/standing-authority work needed to prevent exceptions.
Do not close this workplan until cadence evidence is observed or explicitly handed 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. 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.

View file

@ -4,7 +4,7 @@ type: workplan
title: "Remove recurring deployment exceptions through governed GitOps adoption" title: "Remove recurring deployment exceptions through governed GitOps adoption"
domain: infotech domain: infotech
repo: activity-core repo: activity-core
status: active status: blocked
owner: codex owner: codex
topic_slug: activity-core topic_slug: activity-core
created: "2026-09-27" created: "2026-09-27"
@ -44,7 +44,7 @@ server-dry-run the proposed managed set; no unintended spec change or pruning.
```task ```task
id: ACTIVITY-WP-0041-T02 id: ACTIVITY-WP-0041-T02
status: progress status: wait
priority: high priority: high
state_hub_task_id: "af09865d-8cc9-5571-9c34-20ae679a1e4e" state_hub_task_id: "af09865d-8cc9-5571-9c34-20ae679a1e4e"
``` ```
@ -67,7 +67,7 @@ one-time governance decision.
```task ```task
id: ACTIVITY-WP-0041-T03 id: ACTIVITY-WP-0041-T03
status: progress status: wait
priority: high priority: high
state_hub_task_id: "8e2b4ed9-d455-5367-85d9-8e4222ce75f3" 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, verification, real build/review/retention issuers, production invariant probes,
Temporal observer/dispatcher registration and evidence sinks. The implemented Temporal observer/dispatcher registration and evidence sinks. The implemented
observer cannot retroactively assert the earlier soak interval. See docs/release-broker.md. 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.