feat: Add streaming log endpoint for Actions workflow runs#37515
Draft
rossigee wants to merge 6 commits intogo-gitea:mainfrom
Draft
feat: Add streaming log endpoint for Actions workflow runs#37515rossigee wants to merge 6 commits intogo-gitea:mainfrom
rossigee wants to merge 6 commits intogo-gitea:mainfrom
Conversation
- Cancel and approve workflow runs via POST /runs/{run}/cancel|approve
- Download all job logs as zip via GET /runs/{run}/logs
- Download individual job log via GET /runs/{run}/jobs/{job_id}/logs
- Stream live log cursors via POST /runs/{run}/logs
- Add CreatedAt field to ActionWorkflowRun API response
- Extract shared log streaming and cancel logic into services/actions
- Move streaming log types to modules/structs
- Add Swagger documentation for all new endpoints
- Add integration tests with subtests for all new endpoints
Co-Authored-By: Claude Sonnet 4.6 <claude-sonnet-4-6@anthropic.com>
- Cast org.Visibility.String() to api.UserVisibility in ToOrganization - Cast t.AccessMode.ToString() to api.AccessLevelName in ToTeams - Update webhook notifier to pass repo to ToActionWorkflowRun Co-Authored-By: Claude Sonnet 4.6 <claude-sonnet-4-6@anthropic.com>
Pass repo parameter to ToActionWorkflowRun in action.go and shared/action.go. Co-Authored-By: Claude Sonnet 4.6 <claude-sonnet-4-6@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <claude-sonnet-4-6@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <claude-sonnet-4-6@anthropic.com>
- Add reqToken/reqRepoReader to GET log download endpoints for consistency
with the POST streaming endpoint
- Remove spurious LoadRepos call in DownloadActionsRunAllJobLogs; jobs are
already scoped to the repo by the query and Repo is never read
- Refactor reader.Close() in zip loop to use a closure with defer
- Update copyright year to 2026 on new services/actions/{cancel,log}.go
- Add TestAPIActionsListUserWorkflows and TestAPIActionsListRepoWorkflows
as standalone top-level tests (were dropped when breaking up the
orchestrator)
- Add idempotency assertion to TestAPIActionsApproveWorkflowRun: approving
an already-approved run returns 400
Co-Authored-By: Claude Sonnet <claude-sonnet-4-6@anthropic.com>
Member
|
This seems useful for CLI use cases, similar to |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds a cursor-based streaming logs API for Actions workflow runs (to support incremental polling per step), and—due to the dependency on #35382—also includes related workflow run management/log download endpoints plus supporting conversions, swagger updates, and integration tests.
Changes:
- Add
POST /repos/{owner}/{repo}/actions/runs/{run}/logsstreaming endpoint returning per-step incremental log lines based on cursors. - Add run/job log download endpoints and run management endpoints (cancel/approve), and wire routes + swagger.
- Extend Actions API structs/conversion (including
created_at) and update integration/unit tests accordingly.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
services/actions/log.go |
New ReadStepLogs service for cursor-based step log reads. |
routers/api/v1/repo/actions_run.go |
Implements cancel/approve, run logs download, job logs download, and streaming logs handlers. |
routers/common/actions.go |
Adds helper to zip/download all job logs; improves job log download error mapping. |
modules/structs/repo_actions.go |
Adds streaming log request/response structs; adds created_at to workflow run struct. |
services/convert/convert.go |
Updates Actions conversions (new ToActionWorkflowRun signature; adds CreatedAt). |
services/actions/cancel.go |
New service for cancelling runs by cancelling jobs and notifying downstream components. |
routers/api/v1/api.go |
Wires new Actions run routes (/cancel, /approve, /logs, job log download). |
templates/swagger/v1_openapi3_json.tmpl |
Documents new endpoints + adds created_at field to schema. |
templates/swagger/v1_json.tmpl |
Swagger v2 docs for new endpoints + adds created_at field to schema. |
tests/integration/api_actions_run_test.go |
Expands integration coverage for new Actions endpoints, including streaming logs. |
services/actions/notifier.go |
Adapts to new ToActionWorkflowRun signature. |
services/webhook/notifier.go |
Adapts to new ToActionWorkflowRun signature. |
routers/api/v1/shared/action.go |
Adapts list-runs conversion call to new ToActionWorkflowRun signature. |
routers/api/v1/repo/action.go |
Adapts existing run endpoints to new ToActionWorkflowRun signature. |
services/convert/action_test.go |
Updates unit test for updated workflow run conversion signature. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+26
to
+28
| if cursor.Step >= len(steps) { | ||
| continue | ||
| } |
Comment on lines
+28
to
+33
|
|
||
| CreateCommitStatusForRunJobs(ctx, run, jobs...) | ||
| EmitJobsIfReadyByJobs(updatedJobs) | ||
| NotifyWorkflowJobsStatusUpdate(ctx, updatedJobs...) | ||
| if len(updatedJobs) > 0 { | ||
| NotifyWorkflowRunStatusUpdateWithReload(ctx, run.RepoID, run.ID) |
Comment on lines
+54
to
+61
| // Set headers for zip download | ||
| ctx.Resp.Header().Set("Content-Type", "application/zip") | ||
| ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s-run-%d-logs.zip"`, safeWorkflowName, runID)) | ||
|
|
||
| // Create zip writer | ||
| zipWriter := zip.NewWriter(ctx.Resp) | ||
| defer zipWriter.Close() | ||
|
|
Comment on lines
+77
to
+80
| // Create file in zip with job name and task ID; sanitize to prevent Zip Slip | ||
| safeJobName := strings.NewReplacer("/", "-", `\`, "-", "..", "__").Replace(job.Name) | ||
| fileName := fmt.Sprintf("%s-%s-%d.log", safeWorkflowName, safeJobName, task.ID) | ||
|
|
Comment on lines
+48
to
+52
| workflowName := runJobs[0].Run.WorkflowID | ||
| if p := strings.Index(workflowName, "."); p > 0 { | ||
| workflowName = workflowName[0:p] | ||
| } | ||
| safeWorkflowName := strings.NewReplacer(`"`, "", "\r", "", "\n", "", "/", "-", `\`, "-").Replace(workflowName) |
Comment on lines
+16693
to
+16705
| "responses": { | ||
| "200": { | ||
| "description": "Job logs" | ||
| }, | ||
| "404": { | ||
| "$ref": "#/components/responses/notFound" | ||
| } | ||
| }, | ||
| "summary": "Download job logs as plain text", | ||
| "tags": [ | ||
| "repository" | ||
| ] | ||
| } |
Comment on lines
+16806
to
+16813
| "responses": { | ||
| "200": { | ||
| "description": "Logs archive" | ||
| }, | ||
| "404": { | ||
| "$ref": "#/components/responses/notFound" | ||
| } | ||
| }, |
Comment on lines
+5457
to
+5470
| "responses": { | ||
| "200": { | ||
| "description": "success" | ||
| }, | ||
| "400": { | ||
| "$ref": "#/responses/error" | ||
| }, | ||
| "403": { | ||
| "$ref": "#/responses/forbidden" | ||
| }, | ||
| "404": { | ||
| "$ref": "#/responses/notFound" | ||
| } | ||
| } |
Comment on lines
+5682
to
+5695
| "responses": { | ||
| "200": { | ||
| "description": "success" | ||
| }, | ||
| "400": { | ||
| "$ref": "#/responses/error" | ||
| }, | ||
| "403": { | ||
| "$ref": "#/responses/forbidden" | ||
| }, | ||
| "404": { | ||
| "$ref": "#/responses/notFound" | ||
| } | ||
| } |
| } | ||
|
|
||
| // ToActionTask convert an actions_model.ActionTask to an api.ActionTask | ||
| // ToActionTask convert a actions_model.ActionTask to an api.ActionTask |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a cursor-based streaming log endpoint for Actions workflow runs, allowing the UI and API consumers to poll step logs incrementally without downloading the full zip archive.
Note: This PR depends on #35382 (cancel, approve, and log download endpoints). Once that merges, the diff here will shrink to only the streaming-specific changes.
New endpoint
Accepts a JSON body with per-step cursor positions and returns new log lines since each cursor:
{ "logCursors": [ { "step": 0, "cursor": 0, "expanded": true }, { "step": 1, "cursor": 42, "expanded": true } ] }Returns:
{ "stepsLog": [ { "step": 0, "cursor": 5, "lines": [ { "index": 1, "message": "...", "timestamp": 1234567890.123 } ], "started": 1234567890 } ] }Design note
POST is used rather than GET because the request carries a structured array of per-step cursor state that doesn't map cleanly to query parameters (one cursor per expanded step, variable number). Open to discussion — if a simpler single-cursor GET endpoint is preferred, that can be explored here.
New files
services/actions/log.go—ReadStepLogsservice functionmodules/structs/repo_actions.go:ActionLogCursor,ActionLogRequest,ActionLogStepLine,ActionLogStep,ActionLogResponseTest plan
TestAPIActionsGetWorkflowRunLogsStreampasses (empty cursors, cursor with step, not-found)make lint-go— 0 issuesCo-Authored-By: Ross Golder ross@golder.org