mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-13 11:05:46 +02:00
start
This commit is contained in:
parent
abcfa53040
commit
3e295bafd0
108
models/actions/run_job_summary.go
Normal file
108
models/actions/run_job_summary.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// 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"
|
||||||
|
|
||||||
|
// 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 rendered 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 = "text/markdown"
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@ -409,6 +409,7 @@ func prepareMigrationTasks() []*migration {
|
|||||||
// Gitea 1.26.0 ends at migration ID number 330 (database version 331)
|
// 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(331, "Add ActionRunAttempt model and related action fields", v1_27.AddActionRunAttemptModel),
|
||||||
|
newMigration(332, "Add ActionRunJobSummary table", v1_27.AddActionRunJobSummaryTable),
|
||||||
}
|
}
|
||||||
return preparedMigrations
|
return preparedMigrations
|
||||||
}
|
}
|
||||||
|
|||||||
16
models/migrations/v1_27/v332.go
Normal file
16
models/migrations/v1_27/v332.go
Normal file
@ -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))
|
||||||
|
}
|
||||||
@ -121,6 +121,9 @@ func ArtifactsRoutes(prefix string) *web.Router {
|
|||||||
m.Get("/{artifact_id}/download", r.downloadArtifact)
|
m.Get("/{artifact_id}/download", r.downloadArtifact)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Job summary upload endpoint (GITHUB_STEP_SUMMARY).
|
||||||
|
m.Put(jobSummaryRouteBase, uploadJobSummary)
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
93
routers/api/actions/job_summary.go
Normal file
93
routers/api/actions/job_summary.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
"code.gitea.io/gitea/modules/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
const jobSummaryRouteBase = "/_apis/pipelines/workflows/{run_id}/jobs/{job_id}/summary"
|
||||||
|
|
||||||
|
func JobSummaryRoutes(prefix string) *web.Router {
|
||||||
|
m := web.NewRouter()
|
||||||
|
m.AfterRouting(ArtifactContexter())
|
||||||
|
|
||||||
|
m.Put(jobSummaryRouteBase, uploadJobSummary)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
ctx.HTTPError(http.StatusInternalServerError, "read request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(body) == 0 {
|
||||||
|
ctx.JSON(http.StatusOK, map[string]string{"message": "empty"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := ctx.Req.Header.Get("Content-Type")
|
||||||
|
if contentType == "" || strings.HasPrefix(contentType, "application/octet-stream") {
|
||||||
|
contentType = "text/markdown"
|
||||||
|
} else {
|
||||||
|
// Strip charset to keep storage normalized; we only store UTF-8 text content.
|
||||||
|
if i := strings.Index(contentType, ";"); i > 0 {
|
||||||
|
contentType = strings.TrimSpace(contentType[:i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@ -118,7 +118,7 @@ func (s *Service) Declare(
|
|||||||
return nil, status.Errorf(codes.Internal, "update runner: %v", err)
|
return nil, status.Errorf(codes.Internal, "update runner: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return connect.NewResponse(&runnerv1.DeclareResponse{
|
resp := connect.NewResponse(&runnerv1.DeclareResponse{
|
||||||
Runner: &runnerv1.Runner{
|
Runner: &runnerv1.Runner{
|
||||||
Id: runner.ID,
|
Id: runner.ID,
|
||||||
Uuid: runner.UUID,
|
Uuid: runner.UUID,
|
||||||
@ -127,7 +127,11 @@ func (s *Service) Declare(
|
|||||||
Version: runner.Version,
|
Version: runner.Version,
|
||||||
Labels: runner.AgentLabels,
|
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", "job-summary")
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchTask assigns a task to the runner
|
// FetchTask assigns a task to the runner
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import (
|
|||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/templates"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
@ -90,6 +91,7 @@ func MockActionsRunsJobs(ctx *context.Context) {
|
|||||||
resp.State.Run.WorkflowID = "workflow-id"
|
resp.State.Run.WorkflowID = "workflow-id"
|
||||||
resp.State.Run.WorkflowLink = "./workflow-link"
|
resp.State.Run.WorkflowLink = "./workflow-link"
|
||||||
resp.State.Run.TriggerEvent = "push"
|
resp.State.Run.TriggerEvent = "push"
|
||||||
|
renderUtils := templates.NewRenderUtils(ctx)
|
||||||
resp.State.Run.Commit = actions.ViewCommit{
|
resp.State.Run.Commit = actions.ViewCommit{
|
||||||
ShortSha: "ccccdddd",
|
ShortSha: "ccccdddd",
|
||||||
Link: "./commit-link",
|
Link: "./commit-link",
|
||||||
@ -185,6 +187,22 @@ func MockActionsRunsJobs(ctx *context.Context) {
|
|||||||
resp.State.Run.CanRerun = runID == 30 && isLatestAttempt
|
resp.State.Run.CanRerun = runID == 30 && isLatestAttempt
|
||||||
resp.State.Run.CanRerunFailed = 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{
|
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
|
||||||
Name: "artifact-a",
|
Name: "artifact-a",
|
||||||
Size: 100 * 1024,
|
Size: 100 * 1024,
|
||||||
|
|||||||
@ -300,6 +300,8 @@ type ViewResponse struct {
|
|||||||
Duration string `json:"duration"`
|
Duration string `json:"duration"`
|
||||||
TriggeredAt int64 `json:"triggeredAt"` // unix seconds for relative time
|
TriggeredAt int64 `json:"triggeredAt"` // unix seconds for relative time
|
||||||
TriggerEvent string `json:"triggerEvent"` // e.g. pull_request, push, schedule
|
TriggerEvent string `json:"triggerEvent"` // e.g. pull_request, push, schedule
|
||||||
|
|
||||||
|
JobSummaries []*ViewJobSummary `json:"jobSummaries,omitempty"`
|
||||||
} `json:"run"`
|
} `json:"run"`
|
||||||
CurrentJob struct {
|
CurrentJob struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
@ -323,6 +325,13 @@ type ViewJob struct {
|
|||||||
Needs []string `json:"needs,omitempty"`
|
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 {
|
type ViewRunAttempt struct {
|
||||||
Attempt int64 `json:"attempt"`
|
Attempt int64 `json:"attempt"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
@ -497,6 +506,35 @@ func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse,
|
|||||||
}
|
}
|
||||||
resp.State.Run.TriggerEvent = run.TriggerEvent
|
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 {
|
||||||
|
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,
|
// 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.
|
// so passing 0 here scopes to this run's legacy artifacts only.
|
||||||
var runAttemptID int64
|
var runAttemptID int64
|
||||||
|
|||||||
@ -63,6 +63,8 @@ jobs:
|
|||||||
task2 := runner2.fetchTask(t)
|
task2 := runner2.fetchTask(t)
|
||||||
_, job2, run2 := getTaskAndJobAndRunByTaskID(t, task2.Id)
|
_, 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))
|
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo1.Name, run1.ID))
|
||||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
@ -75,6 +77,9 @@ jobs:
|
|||||||
viewResp := DecodeJSON(t, resp, &actions_web.ViewResponse{})
|
viewResp := DecodeJSON(t, resp, &actions_web.ViewResponse{})
|
||||||
assert.Len(t, viewResp.State.Run.Jobs, 1)
|
assert.Len(t, viewResp.State.Run.Jobs, 1)
|
||||||
assert.Equal(t, job1.ID, viewResp.State.Run.Jobs[0].ID)
|
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
|
// 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))
|
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run2.ID, job2.ID))
|
||||||
|
|||||||
@ -53,6 +53,7 @@ onBeforeUnmount(() => {
|
|||||||
<span>{{ locale.status[run.status] }}</span> • <span>{{ locale.totalDuration }} {{ run.duration || '–' }}</span>
|
<span>{{ locale.status[run.status] }}</span> • <span>{{ locale.totalDuration }} {{ run.duration || '–' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<WorkflowGraph
|
<WorkflowGraph
|
||||||
v-if="run.jobs.length > 0"
|
v-if="run.jobs.length > 0"
|
||||||
:store="store"
|
:store="store"
|
||||||
@ -81,4 +82,5 @@ onBeforeUnmount(() => {
|
|||||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||||
background: var(--color-box-header);
|
background: var(--color-box-header);
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -110,6 +110,7 @@ export function createEmptyActionsRun(): ActionsRun {
|
|||||||
triggeredAt: 0,
|
triggeredAt: 0,
|
||||||
triggerEvent: '',
|
triggerEvent: '',
|
||||||
jobs: [] as Array<ActionsJob>,
|
jobs: [] as Array<ActionsJob>,
|
||||||
|
jobSummaries: [],
|
||||||
commit: {
|
commit: {
|
||||||
localeCommit: '',
|
localeCommit: '',
|
||||||
localePushedBy: '',
|
localePushedBy: '',
|
||||||
|
|||||||
@ -206,18 +206,36 @@ async function deleteArtifact(name: string) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="action-view-right">
|
<div class="action-view-right">
|
||||||
<ActionRunSummaryView
|
<template v-if="!props.jobId">
|
||||||
v-if="!props.jobId"
|
<div class="action-view-right-panel">
|
||||||
:store="store"
|
<ActionRunSummaryView
|
||||||
:locale="locale"
|
:store="store"
|
||||||
/>
|
:locale="locale"
|
||||||
<ActionRunJobView
|
/>
|
||||||
v-else
|
</div>
|
||||||
:store="store"
|
<div v-if="run.jobSummaries?.length" class="action-view-right-panel job-summary-section">
|
||||||
:locale="locale"
|
<div class="job-summary-section-header">
|
||||||
:actions-view-url="props.actionsViewUrl"
|
{{ locale.jobSummaries ?? 'Job summaries' }}
|
||||||
:job-id="props.jobId"
|
</div>
|
||||||
/>
|
<div class="job-summary-list">
|
||||||
|
<div v-for="s in run.jobSummaries" :key="s.jobId" class="job-summary-item">
|
||||||
|
<div class="job-summary-header">
|
||||||
|
<strong class="gt-ellipsis">{{ s.jobName || `Job ${s.jobId}` }}</strong>
|
||||||
|
</div>
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<div class="markup job-summary-body" v-html="s.summaryHTML"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="action-view-right-panel">
|
||||||
|
<ActionRunJobView
|
||||||
|
:store="store"
|
||||||
|
:locale="locale"
|
||||||
|
:actions-view-url="props.actionsViewUrl"
|
||||||
|
:job-id="props.jobId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -342,25 +360,32 @@ async function deleteArtifact(name: string) {
|
|||||||
width: 70%;
|
width: 70%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-view-right-panel {
|
||||||
border: 1px solid var(--color-console-border);
|
border: 1px solid var(--color-console-border);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
background: var(--color-console-bg);
|
background: var(--color-console-bg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* begin fomantic button overrides */
|
/* begin fomantic button overrides */
|
||||||
|
|
||||||
.action-view-right .ui.button,
|
.action-view-right-panel .ui.button,
|
||||||
.action-view-right .ui.button:focus {
|
.action-view-right-panel .ui.button:focus {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--color-console-fg-subtle);
|
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);
|
background: var(--color-console-hover-bg);
|
||||||
color: var(--color-console-fg);
|
color: var(--color-console-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-view-right .ui.button:active {
|
.action-view-right-panel .ui.button:active {
|
||||||
background: var(--color-console-active-bg);
|
background: var(--color-console-active-bg);
|
||||||
color: var(--color-console-fg);
|
color: var(--color-console-fg);
|
||||||
}
|
}
|
||||||
@ -378,4 +403,39 @@ async function deleteArtifact(name: string) {
|
|||||||
max-width: none;
|
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);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export type ActionsRun = {
|
|||||||
triggeredAt: number,
|
triggeredAt: number,
|
||||||
triggerEvent: string,
|
triggerEvent: string,
|
||||||
jobs: Array<ActionsJob>,
|
jobs: Array<ActionsJob>,
|
||||||
|
jobSummaries?: Array<ActionsJobSummary>,
|
||||||
commit: {
|
commit: {
|
||||||
localeCommit: string,
|
localeCommit: string,
|
||||||
localePushedBy: string,
|
localePushedBy: string,
|
||||||
@ -41,6 +42,13 @@ export type ActionsRun = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ActionsJobSummary = {
|
||||||
|
jobId: number;
|
||||||
|
jobName: string;
|
||||||
|
contentType: string;
|
||||||
|
summaryHTML: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ActionsRunAttempt = {
|
export type ActionsRunAttempt = {
|
||||||
attempt: number;
|
attempt: number;
|
||||||
status: ActionsRunStatus;
|
status: ActionsRunStatus;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user