diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index 7535fbf593..5e9fb9832c 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -228,35 +228,3 @@ type RunDetails struct { RunURL string `json:"run_url"` HTMLURL string `json:"html_url"` } - -// ActionLogCursor represents a cursor position within a step's log -type ActionLogCursor struct { - Step int `json:"step"` - Cursor int64 `json:"cursor"` - Expanded bool `json:"expanded"` -} - -// ActionLogRequest is the request body for the streaming log endpoint -type ActionLogRequest struct { - LogCursors []ActionLogCursor `json:"logCursors"` -} - -// ActionLogStepLine represents a single log line within a step -type ActionLogStepLine struct { - Index int64 `json:"index"` - Message string `json:"message"` - Timestamp float64 `json:"timestamp"` -} - -// ActionLogStep represents log lines for a single step with cursor state -type ActionLogStep struct { - Step int `json:"step"` - Cursor int64 `json:"cursor"` - Lines []*ActionLogStepLine `json:"lines"` - Started int64 `json:"started"` -} - -// ActionLogResponse is the response body for the streaming log endpoint -type ActionLogResponse struct { - StepsLog []*ActionLogStep `json:"stepsLog"` -} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index ede987cd66..3fce86cda9 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1268,10 +1268,7 @@ func Routes() *web.Router { m.Get("/{job_id}/logs", reqToken(), reqRepoReader(unit.TypeActions), repo.GetWorkflowJobLogs) m.Post("/{job_id}/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowJob) }) - m.Group("/logs", func() { - m.Get("", reqToken(), reqRepoReader(unit.TypeActions), repo.GetWorkflowRunLogs) - m.Post("", reqToken(), reqRepoReader(unit.TypeActions), repo.GetWorkflowRunLogsStream) - }) + m.Get("/logs", reqToken(), reqRepoReader(unit.TypeActions), repo.GetWorkflowRunLogs) m.Get("/artifacts", repo.GetArtifactsOfRun) }) }) diff --git a/routers/api/v1/repo/actions_run.go b/routers/api/v1/repo/actions_run.go index e27cd469af..730771c17c 100644 --- a/routers/api/v1/repo/actions_run.go +++ b/routers/api/v1/repo/actions_run.go @@ -5,14 +5,11 @@ package repo import ( "errors" - "io" "net/http" "os" actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/json" - api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/common" actions_service "code.gitea.io/gitea/services/actions" @@ -237,24 +234,6 @@ func getRunJobs(ctx *context.APIContext, run *actions_model.ActionRun) ([]*actio return jobs, nil } -func getRunJobsAndCurrent(ctx *context.APIContext, run *actions_model.ActionRun, jobIndex int64) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob, error) { - jobs, err := getRunJobs(ctx, run) - if err != nil { - return nil, nil, err - } - if len(jobs) == 0 { - return nil, nil, util.ErrNotExist - } - - if jobIndex >= 0 { - if jobIndex >= int64(len(jobs)) { - return nil, nil, util.ErrNotExist - } - return jobs[jobIndex], jobs, nil - } - return jobs[0], jobs, nil -} - func GetWorkflowRunLogs(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/logs repository getWorkflowRunLogs // --- @@ -369,144 +348,3 @@ func GetWorkflowJobLogs(ctx *context.APIContext) { return } } - -func GetWorkflowRunLogsStream(ctx *context.APIContext) { - // swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/logs repository getWorkflowRunLogsStream - // --- - // summary: Get streaming workflow run logs with cursor support - // consumes: - // - application/json - // produces: - // - application/json - // parameters: - // - name: owner - // in: path - // description: owner of the repo - // type: string - // required: true - // - name: repo - // in: path - // description: name of the repository - // type: string - // required: true - // - name: run - // in: path - // description: run ID - // type: integer - // required: true - // - name: job - // in: query - // description: job index (0-based), defaults to first job - // type: integer - // required: false - // - name: body - // in: body - // schema: - // type: object - // properties: - // logCursors: - // type: array - // items: - // type: object - // properties: - // step: - // type: integer - // cursor: - // type: integer - // expanded: - // type: boolean - // responses: - // "200": - // description: Streaming logs - // schema: - // type: object - // properties: - // stepsLog: - // type: array - // items: - // type: object - // properties: - // step: - // type: integer - // cursor: - // type: integer - // lines: - // type: array - // items: - // type: object - // properties: - // index: - // type: integer - // message: - // type: string - // timestamp: - // type: number - // started: - // type: integer - // "404": - // "$ref": "#/responses/notFound" - - _, run, err := getRunID(ctx) - if err != nil { - if errors.Is(err, util.ErrNotExist) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } - return - } - - jobIndex := int64(-1) - if ctx.FormString("job") != "" { - jobIndex = int64(ctx.FormInt("job")) - } - - var req api.ActionLogRequest - if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil { - if errors.Is(err, io.EOF) { - req = api.ActionLogRequest{LogCursors: []api.ActionLogCursor{}} - } else { - ctx.APIError(http.StatusBadRequest, "Invalid request body") - return - } - } - - current, _, err := getRunJobsAndCurrent(ctx, run, jobIndex) - if err != nil { - if errors.Is(err, util.ErrNotExist) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } - return - } - - var task *actions_model.ActionTask - if current.TaskID > 0 { - task, err = actions_model.GetTaskByID(ctx, current.TaskID) - if err != nil { - ctx.APIErrorInternal(err) - return - } - task.Job = current - if err := task.LoadAttributes(ctx); err != nil { - ctx.APIErrorInternal(err) - return - } - } - - response := &api.ActionLogResponse{ - StepsLog: make([]*api.ActionLogStep, 0), - } - - if task != nil { - logs, err := actions_service.ReadStepLogs(ctx, req.LogCursors, task, "Log has expired and is no longer available") - if err != nil { - ctx.APIErrorInternal(err) - return - } - response.StepsLog = append(response.StepsLog, logs...) - } - - ctx.JSON(http.StatusOK, response) -} diff --git a/services/actions/log.go b/services/actions/log.go deleted file mode 100644 index e5206eabe8..0000000000 --- a/services/actions/log.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2026 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package actions - -import ( - "context" - "fmt" - "time" - - actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/modules/actions" - api "code.gitea.io/gitea/modules/structs" -) - -// ReadStepLogs reads log lines for the given cursor positions from a task. -// expiredMessage is used as the log content when the task's logs have expired. -func ReadStepLogs(ctx context.Context, cursors []api.ActionLogCursor, task *actions_model.ActionTask, expiredMessage string) ([]*api.ActionLogStep, error) { - var logs []*api.ActionLogStep - steps := actions.FullSteps(task) - - for _, cursor := range cursors { - if !cursor.Expanded { - continue - } - if cursor.Step >= len(steps) { - continue - } - step := steps[cursor.Step] - - if task.LogExpired { - if cursor.Cursor == 0 { - logs = append(logs, &api.ActionLogStep{ - Step: cursor.Step, - Cursor: 1, - Lines: []*api.ActionLogStepLine{{ - Index: 1, - Message: expiredMessage, - Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second), - }}, - Started: int64(step.Started), - }) - } - continue - } - - logLines := make([]*api.ActionLogStepLine, 0) - index := step.LogIndex + cursor.Cursor - validCursor := cursor.Cursor >= 0 && - // !(cursor.Cursor < step.LogLength) when the frontend tries to fetch the next - // line before it's ready — return same cursor and empty lines to let caller retry. - cursor.Cursor < step.LogLength && - // !(index < len(task.LogIndexes)) when task data is older than step data. - index < int64(len(task.LogIndexes)) - - if validCursor { - length := step.LogLength - cursor.Cursor - offset := task.LogIndexes[index] - logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, length) - if err != nil { - return nil, fmt.Errorf("actions.ReadLogs: %w", err) - } - for i, row := range logRows { - logLines = append(logLines, &api.ActionLogStepLine{ - Index: cursor.Cursor + int64(i) + 1, // 1-based - Message: row.Content, - Timestamp: float64(row.Time.AsTime().UnixNano()) / float64(time.Second), - }) - } - } - - logs = append(logs, &api.ActionLogStep{ - Step: cursor.Step, - Cursor: cursor.Cursor + int64(len(logLines)), - Lines: logLines, - Started: int64(step.Started), - }) - } - return logs, nil -} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 3ec0b4c7c9..65cc5bcc6d 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -5912,121 +5912,6 @@ "$ref": "#/responses/notFound" } } - }, - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "repository" - ], - "summary": "Get streaming workflow run logs with cursor support", - "operationId": "getWorkflowRunLogsStream", - "parameters": [ - { - "type": "string", - "description": "owner of the repo", - "name": "owner", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "name of the repository", - "name": "repo", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "run ID", - "name": "run", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "job index (0-based), defaults to first job", - "name": "job", - "in": "query" - }, - { - "name": "body", - "in": "body", - "schema": { - "type": "object", - "properties": { - "logCursors": { - "type": "array", - "items": { - "type": "object", - "properties": { - "cursor": { - "type": "integer" - }, - "expanded": { - "type": "boolean" - }, - "step": { - "type": "integer" - } - } - } - } - } - } - } - ], - "responses": { - "200": { - "description": "Streaming logs", - "schema": { - "type": "object", - "properties": { - "stepsLog": { - "type": "array", - "items": { - "type": "object", - "properties": { - "cursor": { - "type": "integer" - }, - "lines": { - "type": "array", - "items": { - "type": "object", - "properties": { - "index": { - "type": "integer" - }, - "message": { - "type": "string" - }, - "timestamp": { - "type": "number" - } - } - } - }, - "started": { - "type": "integer" - }, - "step": { - "type": "integer" - } - } - } - } - } - } - }, - "404": { - "$ref": "#/responses/notFound" - } - } } }, "/repos/{owner}/{repo}/actions/runs/{run}/rerun": { diff --git a/templates/swagger/v1_openapi3_json.tmpl b/templates/swagger/v1_openapi3_json.tmpl index b43aa84ca9..5bb49a14e4 100644 --- a/templates/swagger/v1_openapi3_json.tmpl +++ b/templates/swagger/v1_openapi3_json.tmpl @@ -16815,130 +16815,6 @@ "tags": [ "repository" ] - }, - "post": { - "operationId": "getWorkflowRunLogsStream", - "parameters": [ - { - "description": "owner of the repo", - "in": "path", - "name": "owner", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "name of the repository", - "in": "path", - "name": "repo", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "run ID", - "in": "path", - "name": "run", - "required": true, - "schema": { - "type": "integer" - } - }, - { - "description": "job index (0-based), defaults to first job", - "in": "query", - "name": "job", - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "logCursors": { - "items": { - "properties": { - "cursor": { - "type": "integer" - }, - "expanded": { - "type": "boolean" - }, - "step": { - "type": "integer" - } - }, - "type": "object" - }, - "type": "array" - } - }, - "type": "object" - } - } - }, - "x-originalParamName": "body" - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "stepsLog": { - "items": { - "properties": { - "cursor": { - "type": "integer" - }, - "lines": { - "items": { - "properties": { - "index": { - "type": "integer" - }, - "message": { - "type": "string" - }, - "timestamp": { - "type": "number" - } - }, - "type": "object" - }, - "type": "array" - }, - "started": { - "type": "integer" - }, - "step": { - "type": "integer" - } - }, - "type": "object" - }, - "type": "array" - } - }, - "type": "object" - } - } - }, - "description": "Streaming logs" - }, - "404": { - "$ref": "#/components/responses/notFound" - } - }, - "summary": "Get streaming workflow run logs with cursor support", - "tags": [ - "repository" - ] } }, "/repos/{owner}/{repo}/actions/runs/{run}/rerun": { diff --git a/tests/integration/api_actions_run_test.go b/tests/integration/api_actions_run_test.go index 32b5410fd4..983c4b22ef 100644 --- a/tests/integration/api_actions_run_test.go +++ b/tests/integration/api_actions_run_test.go @@ -9,7 +9,6 @@ import ( "net/http" "net/url" "slices" - "strings" "testing" "time" @@ -549,35 +548,3 @@ func TestAPIActionsGetWorkflowJobLogs(t *testing.T) { MakeRequest(t, req, http.StatusNotFound) }) } - -func TestAPIActionsGetWorkflowRunLogsStream(t *testing.T) { - defer prepareTestEnvActionsArtifacts(t)() - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - session := loginUser(t, user.Name) - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - - t.Run("EmptyCursors", func(t *testing.T) { - req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/logs", repo.FullName()), strings.NewReader(`{"logCursors": []}`)). - AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - - var logResp map[string]any - err := json.Unmarshal(resp.Body.Bytes(), &logResp) - assert.NoError(t, err) - assert.Contains(t, logResp, "stepsLog") - }) - - t.Run("WithCursor", func(t *testing.T) { - req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/logs", repo.FullName()), strings.NewReader(`{"logCursors": [{"step": 0, "cursor": 0, "expanded": true}]}`)). - AddTokenAuth(token) - MakeRequest(t, req, http.StatusOK) - }) - - t.Run("NotFound", func(t *testing.T) { - req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/999999/logs", repo.FullName()), strings.NewReader(`{"logCursors": []}`)). - AddTokenAuth(token) - MakeRequest(t, req, http.StatusNotFound) - }) -}