Live streams
Overview
Three things can be watched as they change instead of polled: a merge request, a pipeline run's status, and a running step's log. Each is a stream of server-sent events you open with an ordinary authenticated request; it tells you within seconds when something moves, and a dropped connection resumes exactly where it left off.
A stream speeds up what polling would tell you; it does not replace it. Streams are refused at capacity and closed on a deadline by design, so keep a slower poll as the fallback and the worst case is a little latency rather than a stale page.
How it works
The three streams
| Watch | Path | Data event | Its data | Its id | Scope |
|---|---|---|---|---|---|
| A merge request | /repositories/{repo_id}/merge-requests/{mr_id}/stream | revision | {} — re-read the merge request | an opaque revision | merge_request:read |
| A run's status | /repositories/{repo_id}/pipeline-runs/{pipeline_run_id}/stream | status | the run's whole status | the run's version, such as v17 | pipeline:read |
| A step's log | /repositories/{repo_id}/pipeline-runs/{pipeline_run_id}/nodes/{node_run_id}/logs/stream | lines | an array of log records | the last line number in the frame | pipeline:read |
The merge-request stream carries nothing on purpose: what a merge request page shows depends on who is reading it, and one stream's frames go to everyone watching. When its revision moves, re-read through the ordinary endpoints. It does not notice a push to the target branch, a pipeline result, or an edit to an existing comment's text, so poll for those.
Authenticating
The token is read only from the Authorization: Bearer header — never from the query string, never from a cookie. The browser's built-in EventSource cannot send headers, so it can open a stream only on a public resource without credentials. For anything else, read the stream with fetch and parse the frames yourself.
Frames and events
retry: 5000id: v17event: statusdata: {"status":"running", …} :ping event: enddata: {}| You see | It means | What to do |
|---|---|---|
retry: 5000 on the first frame | the reconnect delay the server suggests (merge-request and run-status streams) | use it, or your own back-off |
:ping every 15 seconds | a keep-alive comment, so a quiet stream is not mistaken for a dead one | ignore it |
revision, status or lines | the resource moved | act on it, and remember its id |
end | it is finished — the run ended, the step's log is complete, or the merge request was deleted | stop; do not reconnect |
reset | the stream cannot continue from here — you fell behind, your access changed, or the connection reached its deadline | re-read through the ordinary endpoint, then reconnect |
A closed merge request is not finished, because it can be reopened, so its stream does not end when it closes.
Resuming
Send the id of the last event you received as the Last-Event-ID header when you reconnect. There is no session on the server: the id is the whole cursor. A merge-request or run-status stream opens without repeating what you already hold. A log stream resumes at the line after the one you name. On a first connection, from_line starts it at a line of your choosing; without either, it starts with the last 500 lines. The header wins when both are sent.
Status codes
| Status | Meaning | What to do |
|---|---|---|
200 | a stream | read it |
204 | finished, and you already hold the last of it — the run's final version, or the step's last line | stop; never retry |
401 | no valid credential | sign in again or use a valid token |
403, 404 | not allowed, or not there | stop; the ordinary read reports the real error |
503 with Retry-After: 30 | the server is watching as many merge requests or runs as it will | wait at least that long, add jitter, and poll meanwhile |
Every connection ends on purpose
A stream is authorized once, when it opens, so every connection has a deadline. A merge-request or run-status stream is closed after 10 to 12 minutes; a log stream after 30, and it re-checks your access every minute in between. Each ends with reset, which is not an error: reconnect with Last-Event-ID, and the reconnect checks your access again. Two people watching the same merge request are cut off at the same moment, so add jitter of your own to the reconnect.
A log stream also counts the bytes it sends toward your egress budget, and ends when that budget runs out.
From the CLI
gitmoap pipeline-runs stream-status --repo-id acme/api --pipeline-run-id <run-id>gitmoap mr stream, gitmoap pipeline-runs stream-status and gitmoap pipeline-runs stream-node-run-logs print frames as they arrive. Replace <run-id> with the run's id. Add --last-event-id with the last id you saw to resume.
Related
- Pipelineswatching a run, following a step's log, and the status poll to fall back on
- Merge requestswatching a merge request, and what its stream does not see
- Cachingthe
ETagthat makes the fallback poll cheap - Errorsthe
503and when to retry