mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-10 09:41:52 +02:00
Merge da9090927182a03338ac563b081854a970c65fb5 into 0a3aaeafe7bef9d6935422f4b91c77c216c01b21
This commit is contained in:
commit
ecd65cd9d1
114
models/actions/run_job_summary.go
Normal file
114
models/actions/run_job_summary.go
Normal file
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
// Job summary upload endpoint (GITHUB_STEP_SUMMARY).
|
||||
m.Put(jobSummaryRouteBase, uploadJobSummary)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
|
||||
96
routers/api/actions/job_summary.go
Normal file
96
routers/api/actions/job_summary.go
Normal file
@ -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
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)()
|
||||
|
||||
|
||||
@ -53,6 +53,7 @@ onBeforeUnmount(() => {
|
||||
<span>{{ locale.status[run.status] }}</span> • <span>{{ locale.totalDuration }} {{ run.duration || '–' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WorkflowGraph
|
||||
v-if="run.jobs.length > 0"
|
||||
:store="store"
|
||||
@ -81,4 +82,5 @@ onBeforeUnmount(() => {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
background: var(--color-box-header);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -110,6 +110,7 @@ export function createEmptyActionsRun(): ActionsRun {
|
||||
triggeredAt: 0,
|
||||
triggerEvent: '',
|
||||
jobs: [] as Array<ActionsJob>,
|
||||
jobSummaries: [],
|
||||
commit: {
|
||||
localeCommit: '',
|
||||
localePushedBy: '',
|
||||
|
||||
@ -206,18 +206,36 @@ async function deleteArtifact(name: string) {
|
||||
</div>
|
||||
|
||||
<div class="action-view-right">
|
||||
<ActionRunSummaryView
|
||||
v-if="!props.jobId"
|
||||
:store="store"
|
||||
:locale="locale"
|
||||
/>
|
||||
<ActionRunJobView
|
||||
v-else
|
||||
:store="store"
|
||||
:locale="locale"
|
||||
:actions-view-url="props.actionsViewUrl"
|
||||
:job-id="props.jobId"
|
||||
/>
|
||||
<template v-if="!props.jobId">
|
||||
<div class="action-view-right-panel">
|
||||
<ActionRunSummaryView
|
||||
:store="store"
|
||||
:locale="locale"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="run.jobSummaries?.length" class="action-view-right-panel job-summary-section">
|
||||
<div class="job-summary-section-header">
|
||||
{{ locale.jobSummaries ?? 'Job summaries' }}
|
||||
</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>
|
||||
@ -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);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -24,6 +24,7 @@ export type ActionsRun = {
|
||||
triggeredAt: number,
|
||||
triggerEvent: string,
|
||||
jobs: Array<ActionsJob>,
|
||||
jobSummaries?: Array<ActionsJobSummary>,
|
||||
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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user