Skip to content

feat: Add streaming log endpoint for Actions workflow runs#37515

Draft
rossigee wants to merge 6 commits intogo-gitea:mainfrom
rossigee:feature/runner-logs-stream-api
Draft

feat: Add streaming log endpoint for Actions workflow runs#37515
rossigee wants to merge 6 commits intogo-gitea:mainfrom
rossigee:feature/runner-logs-stream-api

Conversation

@rossigee
Copy link
Copy Markdown
Contributor

@rossigee rossigee commented May 3, 2026

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

POST /repos/{owner}/{repo}/actions/runs/{run}/logs

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.goReadStepLogs service function
  • New struct types in modules/structs/repo_actions.go: ActionLogCursor, ActionLogRequest, ActionLogStepLine, ActionLogStep, ActionLogResponse

Test plan

  • TestAPIActionsGetWorkflowRunLogsStream passes (empty cursors, cursor with step, not-found)
  • make lint-go — 0 issues

Co-Authored-By: Ross Golder ross@golder.org

rossigee and others added 6 commits May 3, 2026 09:49
- 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>
@GiteaBot GiteaBot added the lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. label May 3, 2026
@silverwind
Copy link
Copy Markdown
Member

This seems useful for CLI use cases, similar to gh run watch <run-id>, maybe something to add to tea cli.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}/logs streaming 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 thread services/actions/log.go
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 thread routers/common/actions.go
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 thread routers/common/actions.go
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 thread routers/common/actions.go
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants