Release Orchestrator · First Signal API
Provides a fast “first meaningful signal” for a run — the time-to-first-signal (TTFS) read — backed by a per-run snapshot, with weak-ETag conditional requests.
Audience: UI and CLI authors polling run progress, and integrators wiring early run telemetry.
Implemented by the Release Orchestrator service (
StellaOps.ReleaseOrchestrator.WebApi,FirstSignalEndpoints→FirstSignalService). The signal is snapshot-only: this service does not compute signals from runs/jobs. It returns whatever snapshot an upstream producer has written viaFirstSignalService.UpdateSnapshotAsync(persisted inrelease_orchestrator.first_signal_snapshots). There is no in-process cache layer, no cold-start computation path, and no background snapshot writer in this service.
Endpoint
Canonical route:
GET /api/v1/release-orchestrator/runs/{runId}/first-signal
Legacy compatibility alias (the gateway still routes UI /api/v1/jobengine/* paths to this service; JobEngineLegacyEndpoints registers the alias and delegates to the same handler):
GET /api/v1/jobengine/runs/{runId}/first-signal
{runId} is constrained to a GUID ({runId:guid}); a non-GUID segment does not match this route.
Authorization
- Scope required:
orch:read(policyReleaseOrchestratorPolicies.Read). Requests without a bearer token carryingorch:readare rejected by the authorization layer (401/403). - The endpoint also applies
RequireTenant(), so a tenant context must be present in addition to the header below.
Required headers
X-Tenant-Id: tenant identifier (string). Resolved byTenantResolver; a missing or blank value yields400 Bad Request.
Optional headers
If-None-Match: weak ETag from a previous200response. Multiple comma-separated values are supported, and*matches any current ETag.
Responses
200 OK
Returns the first signal payload and a weak ETag.
Response headers:
ETag: weak ETag (echo intoIf-None-Matchon the next request). Only set when a signal is present.Cache-Control: private, max-age=60(only set whenETagis set).Cache-Status: hit|miss. In the snapshot-only service this is alwaysmiss(there is no in-memory cache;CacheHitis alwaysfalse).X-FirstSignal-Source: diagnostic source tag. In this service the value is alwayssnapshot.
Body (application/json) — shape of FirstSignalResponse / FirstSignalDto:
{
"runId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"firstSignal": {
"type": "started",
"stage": "resolve",
"step": null,
"message": "Run started",
"at": "2025-12-15T12:00:10+00:00",
"artifact": { "kind": "image", "range": null },
"lastKnownOutcome": null
},
"summaryEtag": "W/\"...\""
}
Field notes (grounded in FirstSignalEndpoints.MapToResponse and FirstSignalDto):
runId— the run GUID from the route.firstSignal.type— lower-casedFirstSignalKind: one ofqueued,started,phase,blocked,failed,succeeded,canceled,unavailable.firstSignal.stage— lower-casedFirstSignalPhase: one ofresolve,fetch,restore,analyze,policy,report,unknown. Nullable.firstSignal.step— alwaysnullin the current implementation (reserved).firstSignal.message—signal.Summary.firstSignal.at—signal.Timestamp(DateTimeOffset).firstSignal.artifact.kind—signal.Scope.Type:repo,image, orartifact.firstSignal.artifact.range—{ "start": <int>, "end": <int> }ornull(never populated by the current mapper, which always emitsrange: null).firstSignal.lastKnownOutcome—null, or an object withsignatureId,errorCode,token,excerpt,confidence(low|medium|high),firstSeenAt, andhitCount.summaryEtag— the same weak ETag as theETagresponse header (empty string if no ETag).
Note: when a signal is present,
firstSignalis non-null.firstSignalis serialized asnullonly via theMapToResponsebranch whereresult.Signal is null, which is not reachable for a200in the currentFirstSignalService(a found snapshot always carries a deserialized signal).
304 Not Modified
Returned when If-None-Match matches the current weak ETag (exact match, or *).
404 Not Found
Returned when no first-signal snapshot exists for the resolved tenant + run, or when the stored snapshot JSON is unparseable (the service deletes the bad row and returns 404). This is the “no signal available yet” case for the snapshot-only service.
400 Bad Request
Missing/blank X-Tenant-Id header, an empty run GUID, or other invalid argument. Body shape: { "error": "<message>" }.
Not currently returned:
204 No Content. The result-status enum defines aNotAvailablestate that maps to204, but the snapshot-onlyFirstSignalServicenever produces it — the absence of a snapshot is reported as404 Not Found. Treat204as reserved/forward-looking.
ETag semantics
- Weak ETags (
W/"<8-byte base64>") are a SHA-256 hash (first 8 bytes, base64-encoded) over a canonical JSON projection of the stable signal content:version,jobId,timestamp,kind,phase,scope,summary,etaSeconds,lastKnownOutcome, andnextActions. - Per-request diagnostics (
diagnostics.cacheHit,diagnostics.source,diagnostics.correlationId) are intentionally excluded from the ETag material.
Stored snapshot model
The snapshot the producer writes (and the GET reads) is persisted in PostgreSQL table release_orchestrator.first_signal_snapshots (migration 001_initial.sql), keyed by (tenant_id, run_id):
| Column | Notes |
|---|---|
tenant_id, run_id | composite primary key |
job_id | run’s job GUID |
created_at, updated_at | timestamps |
kind | CHECK ∈ queued, started, phase, blocked, failed, succeeded, canceled, unavailable |
phase | CHECK ∈ resolve, fetch, restore, analyze, policy, report, unknown |
summary, eta_seconds | scalar fields |
last_known_outcome, next_actions, diagnostics | JSONB |
signal_json | full canonical signal document used for ETag/return |
The underlying FirstSignal domain record also carries version (default "1.0"), signalId, scope { type, id }, and nextActions[] { type, label, target }, which are persisted in signal_json but are not all surfaced in the REST response DTO.
Streaming (SSE) — NOT IMPLEMENTED in this service
The Server-Sent Events stream described in earlier drafts (
GET /api/v1/jobengine/stream/runs/{runId}emittingfirst_signalevents) is not implemented by the Release Orchestrator. The legacy/api/v1/jobengine/stream/*prefix is registered as an unsupported compatibility route inJobEngineLegacyEndpointsand returns501 Not Implementedwith body{ "error": "not_implemented", "message": "...not yet been migrated to the release-orchestrator service.", "path": "...", "hint": "..." }. Run-stream /first_signalpush delivery is tracked in the orchestrator decomposition plan and is not available today.
Configuration
There is no FirstSignal (Cache / ColdPath / SnapshotWriter) or messaging configuration section for this endpoint in the Release Orchestrator. The feature is snapshot-only and Postgres-backed; the relevant configuration is the service’s database connection (the release_orchestrator schema is auto-migrated on startup via AddReleaseOrchestratorPersistence). No in-memory/Valkey cache backend, cold-path timeout, or background snapshot-writer poller is wired in this service.
The
FirstSignal.Cache/FirstSignal.ColdPath/FirstSignal.SnapshotWriterblock documented in earlier drafts belonged to the legacy JobEngine implementation and does not apply to the Release Orchestrator service that now serves these routes.
