From 438ac5acc12855329f6eeb62af7f7a651e8a6a95 Mon Sep 17 00:00:00 2001 From: "Semenets V. Pavel" Date: Fri, 26 Dec 2025 09:28:57 +0300 Subject: [PATCH 1/2] feat: Add workflow dependencies visualization ## API changes: - Add job_id field for YAML job identifiers - Add needs field for job dependencies - Update swagger documentation ## Frontend features: - Interactive graph with zoom/pan navigation - Matrix jobs visualization - Custom job names support via job_id - Status-based coloring and animations - Hover highlighting for related jobs - Click to navigate to job details ## Technical details: - Uses existing database columns (no schema changes) - Backward compatible (omitempty for new fields) - Handles edge cases (cycles, missing dependencies) - Performance optimized ## UI integration: - Updated RepoActionView.vue styles for proper theme compatibility - Consistent styling with Gitea's CSS variables --- modules/structs/repo_actions.go | 2 + routers/web/repo/actions/view.go | 14 +- services/convert/convert.go | 4 +- templates/swagger/v1_json.tmpl | 11 + web_src/js/components/RepoActionView.vue | 114 ++- web_src/js/components/WorkflowGraph.vue | 1192 ++++++++++++++++++++++ 6 files changed, 1302 insertions(+), 35 deletions(-) create mode 100644 web_src/js/components/WorkflowGraph.vue diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index b491d6ccce..fabbc07ea2 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -161,6 +161,7 @@ type ActionWorkflowStep struct { // ActionWorkflowJob represents a WorkflowJob type ActionWorkflowJob struct { ID int64 `json:"id"` + JobID string `json:"job_id,omitempty"` URL string `json:"url"` HTMLURL string `json:"html_url"` RunID int64 `json:"run_id"` @@ -175,6 +176,7 @@ type ActionWorkflowJob struct { RunnerID int64 `json:"runner_id,omitempty"` RunnerName string `json:"runner_name,omitempty"` Steps []*ActionWorkflowStep `json:"steps"` + Needs []string `json:"needs,omitempty"` // swagger:strfmt date-time CreatedAt time.Time `json:"created_at"` // swagger:strfmt date-time diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index cc70cd4e06..e629f866ed 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -142,11 +142,13 @@ type ViewResponse struct { } type ViewJob struct { - ID int64 `json:"id"` - Name string `json:"name"` - Status string `json:"status"` - CanRerun bool `json:"canRerun"` - Duration string `json:"duration"` + ID int64 `json:"id"` + JobID string `json:"job_id,omitempty"` + Name string `json:"name"` + Status string `json:"status"` + CanRerun bool `json:"canRerun"` + Duration string `json:"duration"` + Needs []string `json:"needs,omitempty"` } type ViewCommit struct { @@ -247,10 +249,12 @@ func ViewPost(ctx *context_module.Context) { for _, v := range jobs { resp.State.Run.Jobs = append(resp.State.Run.Jobs, &ViewJob{ ID: v.ID, + JobID: v.JobID, Name: v.Name, Status: v.Status.String(), CanRerun: resp.State.Run.CanRerun, Duration: v.Duration().String(), + Needs: v.Needs, }) } diff --git a/services/convert/convert.go b/services/convert/convert.go index c081aec771..f516cd5805 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -367,7 +367,8 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task } return &api.ActionWorkflowJob{ - ID: job.ID, + ID: job.ID, + JobID: job.JobID, // missing api endpoint for this location URL: fmt.Sprintf("%s/actions/jobs/%d", repo.APIURL(), job.ID), HTMLURL: fmt.Sprintf("%s/jobs/%d", job.Run.HTMLURL(), jobIndex), @@ -384,6 +385,7 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task RunnerID: runnerID, RunnerName: runnerName, Steps: steps, + Needs: job.Needs, CreatedAt: job.Created.AsTime().UTC(), StartedAt: job.Started.AsTime().UTC(), CompletedAt: job.Stopped.AsTime().UTC(), diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index be6c4bdfd3..6851f3a6ee 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -21322,6 +21322,10 @@ "format": "int64", "x-go-name": "ID" }, + "job_id": { + "type": "string", + "x-go-name": "JobID" + }, "labels": { "type": "array", "items": { @@ -21333,6 +21337,13 @@ "type": "string", "x-go-name": "Name" }, + "needs": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "Needs" + }, "run_attempt": { "type": "integer", "format": "int64", diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 357a2ba10e..4cc3d1644d 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -8,6 +8,7 @@ import {renderAnsi} from '../render/ansi.ts'; import {POST, DELETE} from '../modules/fetch.ts'; import type {IntervalId} from '../types.ts'; import {toggleFullScreen} from '../utils.ts'; +import WorkflowGraph from './WorkflowGraph.vue' // see "models/actions/status.go", if it needs to be used somewhere else, move it to a shared file like "types/actions.ts" type RunStatus = 'unknown' | 'waiting' | 'running' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked'; @@ -28,9 +29,11 @@ type LogLineCommand = { type Job = { id: number; + job_id: string; name: string; status: RunStatus; canRerun: boolean; + needs?: string[]; duration: string; } @@ -85,6 +88,7 @@ export default defineComponent({ components: { SvgIcon, ActionRunStatus, + WorkflowGraph }, props: { runIndex: { @@ -115,6 +119,7 @@ export default defineComponent({ artifacts: [] as Array>, menuVisible: false, isFullScreen: false, + showSummary: true, timeVisible: { 'log-time-stamp': false, 'log-time-seconds': false, @@ -512,6 +517,16 @@ export default defineComponent({