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({