diff --git a/models/actions/run_job_summary.go b/models/actions/run_job_summary.go
new file mode 100644
index 0000000000..a523839ee0
--- /dev/null
+++ b/models/actions/run_job_summary.go
@@ -0,0 +1,114 @@
+// Copyright 2026 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+ "context"
+ "errors"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/util"
+)
+
+const (
+ // JobSummaryCapability is the runner-declare capability string for job summaries.
+ JobSummaryCapability = "job-summary"
+
+ // JobSummaryContentTypeMarkdown is the only accepted content type for job summaries.
+ JobSummaryContentTypeMarkdown = "text/markdown"
+
+ // MaxJobSummarySize is the maximum accepted summary payload size in bytes.
+ // This is intentionally conservative to avoid DB bloat and UI abuse.
+ MaxJobSummarySize = 1024 * 1024 // 1 MiB
+)
+
+// ActionRunJobSummary stores the raw job summary markdown uploaded by the runner.
+// It is internal state (not a downloadable artifact).
+type ActionRunJobSummary struct {
+ ID int64 `xorm:"pk autoincr"`
+
+ RepoID int64 `xorm:"UNIQUE(summary_key) INDEX"`
+ RunID int64 `xorm:"UNIQUE(summary_key) INDEX"`
+ RunAttemptID int64 `xorm:"UNIQUE(summary_key) NOT NULL DEFAULT 0 INDEX"`
+ JobID int64 `xorm:"UNIQUE(summary_key) INDEX"`
+
+ Content string `xorm:"LONGTEXT"`
+ ContentType string `xorm:"VARCHAR(255) NOT NULL DEFAULT 'text/markdown'"`
+
+ Created timeutil.TimeStamp `xorm:"created"`
+ Updated timeutil.TimeStamp `xorm:"updated"`
+}
+
+func init() {
+ db.RegisterModel(new(ActionRunJobSummary))
+}
+
+func GetActionRunJobSummary(ctx context.Context, repoID, runID, runAttemptID, jobID int64) (*ActionRunJobSummary, error) {
+ var s ActionRunJobSummary
+ has, err := db.GetEngine(ctx).
+ Where("repo_id=? AND run_id=? AND run_attempt_id=? AND job_id=?", repoID, runID, runAttemptID, jobID).
+ Get(&s)
+ if err != nil {
+ return nil, err
+ }
+ if !has {
+ return nil, util.ErrNotExist
+ }
+ return &s, nil
+}
+
+func UpsertActionRunJobSummary(ctx context.Context, repoID, runID, runAttemptID, jobID int64, contentType string, content []byte) error {
+ if runID <= 0 || jobID <= 0 || repoID <= 0 {
+ return util.ErrInvalidArgument
+ }
+ if len(content) == 0 {
+ // Treat empty summaries as no-op; runner may create SUMMARY.md but never write to it.
+ return nil
+ }
+ if len(content) > MaxJobSummarySize {
+ return util.ErrInvalidArgument
+ }
+ if contentType == "" {
+ contentType = JobSummaryContentTypeMarkdown
+ }
+ if contentType != JobSummaryContentTypeMarkdown {
+ return util.ErrInvalidArgument
+ }
+
+ engine := db.GetEngine(ctx)
+
+ existing, err := GetActionRunJobSummary(ctx, repoID, runID, runAttemptID, jobID)
+ if err != nil && !errors.Is(err, util.ErrNotExist) {
+ return err
+ }
+
+ if existing == nil {
+ _, err := engine.Insert(&ActionRunJobSummary{
+ RepoID: repoID,
+ RunID: runID,
+ RunAttemptID: runAttemptID,
+ JobID: jobID,
+ Content: string(content),
+ ContentType: contentType,
+ })
+ return err
+ }
+
+ existing.Content = string(content)
+ existing.ContentType = contentType
+ _, err = engine.ID(existing.ID).Cols("content", "content_type").Update(existing)
+ return err
+}
+
+func ListActionRunJobSummariesByRunAttempt(ctx context.Context, repoID, runID, runAttemptID int64) ([]*ActionRunJobSummary, error) {
+ var summaries []*ActionRunJobSummary
+ if err := db.GetEngine(ctx).
+ Where("repo_id=? AND run_id=? AND run_attempt_id=?", repoID, runID, runAttemptID).
+ OrderBy("job_id ASC").
+ Find(&summaries); err != nil {
+ return nil, err
+ }
+ return summaries, nil
+}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index c3a8f08b5d..f25a22a3dd 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -409,6 +409,7 @@ func prepareMigrationTasks() []*migration {
// Gitea 1.26.0 ends at migration ID number 330 (database version 331)
newMigration(331, "Add ActionRunAttempt model and related action fields", v1_27.AddActionRunAttemptModel),
+ newMigration(332, "Add ActionRunJobSummary table", v1_27.AddActionRunJobSummaryTable),
}
return preparedMigrations
}
diff --git a/models/migrations/v1_27/v332.go b/models/migrations/v1_27/v332.go
new file mode 100644
index 0000000000..f0e9d6b8a0
--- /dev/null
+++ b/models/migrations/v1_27/v332.go
@@ -0,0 +1,16 @@
+// Copyright 2026 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_27
+
+import (
+ "context"
+
+ "code.gitea.io/gitea/models/actions"
+
+ "xorm.io/xorm"
+)
+
+func AddActionRunJobSummaryTable(ctx context.Context, x *xorm.Engine) error {
+ return x.Sync(new(actions.ActionRunJobSummary))
+}
diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go
index 838ddb7f91..263fbcefa2 100644
--- a/routers/api/actions/artifacts.go
+++ b/routers/api/actions/artifacts.go
@@ -121,6 +121,9 @@ func ArtifactsRoutes(prefix string) *web.Router {
m.Get("/{artifact_id}/download", r.downloadArtifact)
})
+ // Job summary upload endpoint (GITHUB_STEP_SUMMARY).
+ m.Put(jobSummaryRouteBase, uploadJobSummary)
+
return m
}
diff --git a/routers/api/actions/job_summary.go b/routers/api/actions/job_summary.go
new file mode 100644
index 0000000000..cff54c787e
--- /dev/null
+++ b/routers/api/actions/job_summary.go
@@ -0,0 +1,96 @@
+// Copyright 2026 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+ "errors"
+ "io"
+ "mime"
+ "net/http"
+ "strconv"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/util"
+)
+
+const jobSummaryRouteBase = "/_apis/pipelines/workflows/{run_id}/jobs/{job_id}/summary"
+
+func uploadJobSummary(ctx *ArtifactContext) {
+ task, runID, ok := validateRunID(ctx)
+ if !ok {
+ return
+ }
+
+ jobID := ctx.PathParamInt64("job_id")
+ if jobID <= 0 {
+ ctx.HTTPError(http.StatusBadRequest, "invalid job_id")
+ return
+ }
+
+ if task == nil || task.Job == nil {
+ ctx.HTTPError(http.StatusInternalServerError, "task/job not loaded")
+ return
+ }
+ if task.Job.ID != jobID {
+ ctx.HTTPError(http.StatusBadRequest, "job_id mismatch")
+ return
+ }
+ if task.Job.RunID != runID {
+ ctx.HTTPError(http.StatusBadRequest, "run_id mismatch")
+ return
+ }
+
+ body, err := io.ReadAll(io.LimitReader(ctx.Req.Body, actions_model.MaxJobSummarySize+1))
+ if err != nil {
+ log.Error("Error reading job summary request body: %v", err)
+ ctx.HTTPError(http.StatusInternalServerError, "read request body")
+ return
+ }
+ if len(body) == 0 {
+ ctx.JSON(http.StatusOK, map[string]string{"message": "empty"})
+ return
+ }
+
+ contentType, ok := normalizeJobSummaryContentType(ctx.Req.Header.Get("Content-Type"))
+ if !ok {
+ ctx.HTTPError(http.StatusBadRequest, "invalid summary content type")
+ return
+ }
+
+ if err := actions_model.UpsertActionRunJobSummary(ctx, task.Job.RepoID, task.Job.RunID, task.Job.RunAttemptID, task.Job.ID, contentType, body); err != nil {
+ if errorsIsInvalidArg(err) {
+ ctx.HTTPError(http.StatusBadRequest, "invalid summary")
+ return
+ }
+ log.Error("Error upsert job summary: %v", err)
+ ctx.HTTPError(http.StatusInternalServerError, "Error upsert job summary")
+ return
+ }
+
+ ctx.JSON(http.StatusOK, map[string]string{
+ "message": "success",
+ "sizeBytes": strconv.Itoa(len(body)),
+ "runAttempt": strconv.FormatInt(task.Job.RunAttemptID, 10),
+ })
+}
+
+func errorsIsInvalidArg(err error) bool {
+ return errors.Is(err, util.ErrInvalidArgument)
+}
+
+func normalizeJobSummaryContentType(contentType string) (string, bool) {
+ if contentType == "" || contentType == "application/octet-stream" {
+ return actions_model.JobSummaryContentTypeMarkdown, true
+ }
+
+ mediaType, _, err := mime.ParseMediaType(contentType)
+ if err != nil {
+ return "", false
+ }
+ if mediaType != actions_model.JobSummaryContentTypeMarkdown {
+ return "", false
+ }
+ return actions_model.JobSummaryContentTypeMarkdown, true
+}
diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go
index eee39760ed..e785f4d0f9 100644
--- a/routers/api/actions/runner/runner.go
+++ b/routers/api/actions/runner/runner.go
@@ -118,7 +118,7 @@ func (s *Service) Declare(
return nil, status.Errorf(codes.Internal, "update runner: %v", err)
}
- return connect.NewResponse(&runnerv1.DeclareResponse{
+ resp := connect.NewResponse(&runnerv1.DeclareResponse{
Runner: &runnerv1.Runner{
Id: runner.ID,
Uuid: runner.UUID,
@@ -127,7 +127,11 @@ func (s *Service) Declare(
Version: runner.Version,
Labels: runner.AgentLabels,
},
- }), nil
+ })
+ // Capabilities are communicated via headers to avoid a hard dependency on a proto bump.
+ // Older runners ignore unknown headers; newer runners can use this for feature negotiation.
+ resp.Header().Set("X-Gitea-Actions-Capabilities", actions_model.JobSummaryCapability)
+ return resp, nil
}
// FetchTask assigns a task to the runner
diff --git a/routers/web/devtest/mock_actions.go b/routers/web/devtest/mock_actions.go
index 83e9bef9c8..56b7c91cc4 100644
--- a/routers/web/devtest/mock_actions.go
+++ b/routers/web/devtest/mock_actions.go
@@ -15,6 +15,7 @@ import (
actions_model "code.gitea.io/gitea/models/actions"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
@@ -90,6 +91,7 @@ func MockActionsRunsJobs(ctx *context.Context) {
resp.State.Run.WorkflowID = "workflow-id"
resp.State.Run.WorkflowLink = "./workflow-link"
resp.State.Run.TriggerEvent = "push"
+ renderUtils := templates.NewRenderUtils(ctx)
resp.State.Run.Commit = actions.ViewCommit{
ShortSha: "ccccdddd",
Link: "./commit-link",
@@ -185,6 +187,22 @@ func MockActionsRunsJobs(ctx *context.Context) {
resp.State.Run.CanRerun = runID == 30 && isLatestAttempt
resp.State.Run.CanRerunFailed = runID == 30 && isLatestAttempt
+ // Mock job summaries so the devtest page can preview the Summary panel rendering.
+ resp.State.Run.JobSummaries = []*actions.ViewJobSummary{
+ {
+ JobID: runID * 10,
+ JobName: "job 100 (testsubname)",
+ ContentType: "text/markdown",
+ SummaryHTML: renderUtils.MarkdownToHtml("### Devtest job summary\n\n- Markdown rendering\n- Links: [example](https://example.com)\n\n```sh\necho hello\n```\n"),
+ },
+ {
+ JobID: runID*10 + 2,
+ JobName: "ULTRA LOOOOOOOOOOOONG job name 102 that exceeds the limit",
+ ContentType: "text/markdown",
+ SummaryHTML: renderUtils.MarkdownToHtml("### Another summary\n\nThis demonstrates multiple job summaries in one run.\n\n- Item A\n- Item B\n"),
+ },
+ }
+
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
Name: "artifact-a",
Size: 100 * 1024,
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 726ab63e56..a7e765f8d9 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -300,6 +300,8 @@ type ViewResponse struct {
Duration string `json:"duration"`
TriggeredAt int64 `json:"triggeredAt"` // unix seconds for relative time
TriggerEvent string `json:"triggerEvent"` // e.g. pull_request, push, schedule
+
+ JobSummaries []*ViewJobSummary `json:"jobSummaries,omitempty"`
} `json:"run"`
CurrentJob struct {
Title string `json:"title"`
@@ -323,6 +325,13 @@ type ViewJob struct {
Needs []string `json:"needs,omitempty"`
}
+type ViewJobSummary struct {
+ JobID int64 `json:"jobId"`
+ JobName string `json:"jobName"`
+ ContentType string `json:"contentType"`
+ SummaryHTML template.HTML `json:"summaryHTML"`
+}
+
type ViewRunAttempt struct {
Attempt int64 `json:"attempt"`
Status string `json:"status"`
@@ -497,6 +506,39 @@ func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse,
}
resp.State.Run.TriggerEvent = run.TriggerEvent
+ // Job summaries (GITHUB_STEP_SUMMARY). Only show when present.
+ {
+ var runAttemptID int64
+ if attempt != nil {
+ runAttemptID = attempt.ID
+ }
+ summaries, err := actions_model.ListActionRunJobSummariesByRunAttempt(ctx, ctx.Repo.Repository.ID, run.ID, runAttemptID)
+ if err != nil {
+ ctx.ServerError("ListActionRunJobSummariesByRunAttempt", err)
+ return
+ }
+ if len(summaries) > 0 {
+ jobNameByID := make(map[int64]string, len(jobs))
+ for _, j := range jobs {
+ jobNameByID[j.ID] = j.Name
+ }
+ resp.State.Run.JobSummaries = make([]*ViewJobSummary, 0, len(summaries))
+ renderUtils := templates.NewRenderUtils(ctx)
+ for _, s := range summaries {
+ if s.ContentType != actions_model.JobSummaryContentTypeMarkdown {
+ log.Warn("Skip unsupported job summary content type %q for run %d job %d", s.ContentType, s.RunID, s.JobID)
+ continue
+ }
+ resp.State.Run.JobSummaries = append(resp.State.Run.JobSummaries, &ViewJobSummary{
+ JobID: s.JobID,
+ JobName: jobNameByID[s.JobID],
+ ContentType: s.ContentType,
+ SummaryHTML: renderUtils.MarkdownToHtml(s.Content),
+ })
+ }
+ }
+ }
+
// Legacy runs (LatestAttemptID == 0) have no attempt; their artifacts all share run_attempt_id=0,
// so passing 0 here scopes to this run's legacy artifacts only.
var runAttemptID int64
diff --git a/tests/integration/actions_route_test.go b/tests/integration/actions_route_test.go
index 66a00a6773..a4f5fe7e08 100644
--- a/tests/integration/actions_route_test.go
+++ b/tests/integration/actions_route_test.go
@@ -63,6 +63,8 @@ jobs:
task2 := runner2.fetchTask(t)
_, job2, run2 := getTaskAndJobAndRunByTaskID(t, task2.Id)
+ require.NoError(t, actions_model.UpsertActionRunJobSummary(t.Context(), repo1.ID, run1.ID, job1.RunAttemptID, job1.ID, "text/markdown", []byte("### Hello summary\n\nFrom job summary.\n")))
+
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo1.Name, run1.ID))
user2Session.MakeRequest(t, req, http.StatusOK)
@@ -75,6 +77,9 @@ jobs:
viewResp := DecodeJSON(t, resp, &actions_web.ViewResponse{})
assert.Len(t, viewResp.State.Run.Jobs, 1)
assert.Equal(t, job1.ID, viewResp.State.Run.Jobs[0].ID)
+ require.Len(t, viewResp.State.Run.JobSummaries, 1)
+ assert.Equal(t, job1.ID, viewResp.State.Run.JobSummaries[0].JobID)
+ assert.Contains(t, string(viewResp.State.Run.JobSummaries[0].SummaryHTML), "Hello summary")
// run2 and job2 do not belong to repo1, failure
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run2.ID, job2.ID))
diff --git a/tests/integration/api_actions_artifact_test.go b/tests/integration/api_actions_artifact_test.go
index 4670bb0704..c45d38001b 100644
--- a/tests/integration/api_actions_artifact_test.go
+++ b/tests/integration/api_actions_artifact_test.go
@@ -16,6 +16,7 @@ import (
"strings"
"testing"
+ actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
@@ -44,6 +45,67 @@ func prepareTestEnvActionsArtifacts(t *testing.T) func() {
return f
}
+func getArtifactFixtureTask(t *testing.T) *actions_model.ActionTask {
+ t.Helper()
+
+ task, err := actions_model.GetRunningTaskByToken(t.Context(), "8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
+ require.NoError(t, err)
+ require.NoError(t, task.LoadJob(t.Context()))
+ return task
+}
+
+func TestActionsJobSummaryUpload(t *testing.T) {
+ defer prepareTestEnvActionsArtifacts(t)()
+
+ task := getArtifactFixtureTask(t)
+ summaryURL := fmt.Sprintf("/api/actions_pipeline/_apis/pipelines/workflows/%d/jobs/%d/summary", task.Job.RunID, task.Job.ID)
+
+ t.Run("success", func(t *testing.T) {
+ body := "### Uploaded summary\n\n- line one\n"
+ req := NewRequestWithBody(t, "PUT", summaryURL, strings.NewReader(body)).
+ AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
+ SetHeader("Content-Type", "text/markdown; charset=utf-8")
+ MakeRequest(t, req, http.StatusOK)
+
+ summary, err := actions_model.GetActionRunJobSummary(t.Context(), task.Job.RepoID, task.Job.RunID, task.Job.RunAttemptID, task.Job.ID)
+ require.NoError(t, err)
+ assert.Equal(t, actions_model.JobSummaryContentTypeMarkdown, summary.ContentType)
+ assert.Equal(t, body, summary.Content)
+ })
+
+ t.Run("invalid-content-type", func(t *testing.T) {
+ req := NewRequestWithBody(t, "PUT", summaryURL, strings.NewReader("summary")).
+ AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
+ SetHeader("Content-Type", "text/html")
+ resp := MakeRequest(t, req, http.StatusBadRequest)
+ assert.Contains(t, resp.Body.String(), "invalid summary content type")
+ })
+
+ t.Run("size-limit", func(t *testing.T) {
+ req := NewRequestWithBody(t, "PUT", summaryURL, strings.NewReader(strings.Repeat("a", actions_model.MaxJobSummarySize+1))).
+ AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
+ SetHeader("Content-Type", actions_model.JobSummaryContentTypeMarkdown)
+ resp := MakeRequest(t, req, http.StatusBadRequest)
+ assert.Contains(t, resp.Body.String(), "invalid summary")
+ })
+
+ t.Run("job-mismatch", func(t *testing.T) {
+ req := NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/actions_pipeline/_apis/pipelines/workflows/%d/jobs/%d/summary", task.Job.RunID, task.Job.ID+1), strings.NewReader("summary")).
+ AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
+ SetHeader("Content-Type", actions_model.JobSummaryContentTypeMarkdown)
+ resp := MakeRequest(t, req, http.StatusBadRequest)
+ assert.Contains(t, resp.Body.String(), "job_id mismatch")
+ })
+
+ t.Run("run-mismatch", func(t *testing.T) {
+ req := NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/actions_pipeline/_apis/pipelines/workflows/%d/jobs/%d/summary", task.Job.RunID+1, task.Job.ID), strings.NewReader("summary")).
+ AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
+ SetHeader("Content-Type", actions_model.JobSummaryContentTypeMarkdown)
+ resp := MakeRequest(t, req, http.StatusBadRequest)
+ assert.Contains(t, resp.Body.String(), "run-id does not match")
+ })
+}
+
func TestActionsArtifactUploadSingleFile(t *testing.T) {
defer prepareTestEnvActionsArtifacts(t)()
diff --git a/web_src/js/components/ActionRunSummaryView.vue b/web_src/js/components/ActionRunSummaryView.vue
index afbc0a13bf..2485f2dc6a 100644
--- a/web_src/js/components/ActionRunSummaryView.vue
+++ b/web_src/js/components/ActionRunSummaryView.vue
@@ -53,6 +53,7 @@ onBeforeUnmount(() => {
{{ locale.status[run.status] }} • {{ locale.totalDuration }} {{ run.duration || '–' }}
+
{
border-radius: var(--border-radius) var(--border-radius) 0 0;
background: var(--color-box-header);
}
+
diff --git a/web_src/js/components/ActionRunView.ts b/web_src/js/components/ActionRunView.ts
index 91fe8329bd..9a284cefc0 100644
--- a/web_src/js/components/ActionRunView.ts
+++ b/web_src/js/components/ActionRunView.ts
@@ -110,6 +110,7 @@ export function createEmptyActionsRun(): ActionsRun {
triggeredAt: 0,
triggerEvent: '',
jobs: [] as Array,
+ jobSummaries: [],
commit: {
localeCommit: '',
localePushedBy: '',
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 725a996028..73d28bab90 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -206,18 +206,36 @@ async function deleteArtifact(name: string) {
@@ -342,25 +360,32 @@ async function deleteArtifact(name: string) {
width: 70%;
display: flex;
flex-direction: column;
+ gap: 12px;
+}
+
+.action-view-right-panel {
border: 1px solid var(--color-console-border);
border-radius: var(--border-radius);
background: var(--color-console-bg);
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
}
/* begin fomantic button overrides */
-.action-view-right .ui.button,
-.action-view-right .ui.button:focus {
+.action-view-right-panel .ui.button,
+.action-view-right-panel .ui.button:focus {
background: transparent;
color: var(--color-console-fg-subtle);
}
-.action-view-right .ui.button:hover {
+.action-view-right-panel .ui.button:hover {
background: var(--color-console-hover-bg);
color: var(--color-console-fg);
}
-.action-view-right .ui.button:active {
+.action-view-right-panel .ui.button:active {
background: var(--color-console-active-bg);
color: var(--color-console-fg);
}
@@ -378,4 +403,39 @@ async function deleteArtifact(name: string) {
max-width: none;
}
}
+
+.job-summary-section {
+ overflow: hidden;
+}
+
+.job-summary-section-header {
+ padding: 12px;
+ border-bottom: 1px solid var(--color-console-border);
+ background: var(--color-console-bg);
+ color: var(--color-console-fg);
+ font-weight: var(--font-weight-semibold);
+}
+
+.job-summary-list {
+ padding: 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.job-summary-item {
+ padding: 12px;
+ border-radius: var(--border-radius);
+ background: var(--color-console-hover-bg);
+ border: 1px solid var(--color-console-border);
+}
+
+.job-summary-header {
+ color: var(--color-console-fg);
+ margin-bottom: 8px;
+}
+
+.job-summary-body {
+ color: var(--color-console-fg);
+}
diff --git a/web_src/js/modules/gitea-actions.ts b/web_src/js/modules/gitea-actions.ts
index d4cb4b0d1e..be70388075 100644
--- a/web_src/js/modules/gitea-actions.ts
+++ b/web_src/js/modules/gitea-actions.ts
@@ -24,6 +24,7 @@ export type ActionsRun = {
triggeredAt: number,
triggerEvent: string,
jobs: Array,
+ jobSummaries?: Array,
commit: {
localeCommit: string,
localePushedBy: string,
@@ -41,6 +42,13 @@ export type ActionsRun = {
},
};
+export type ActionsJobSummary = {
+ jobId: number;
+ jobName: string;
+ contentType: string;
+ summaryHTML: string;
+};
+
export type ActionsRunAttempt = {
attempt: number;
status: ActionsStatus;