enhance(actions): add pending job status and align job statuses with GitHub (#39376)

This commit is contained in:
silverwind
2026-09-27 20:30:42 +02:00
committed by GitHub
parent 0d09986790
commit 9e7b302bd3
45 changed files with 397 additions and 189 deletions
+3 -4
View File
@@ -1,13 +1,12 @@
<!-- Keep in sync with templates/repo/icons/action_status.tmpl.
action status accepted: success, skipped, waiting, blocked, running, failure, cancelled, cancelling, unknown.
-->
<!-- Keep in sync with templates/repo/icons/action_status.tmpl. -->
<script lang="ts" setup>
import {computed} from 'vue';
import SvgIcon from './SvgIcon.vue';
import {getActionStatusIcon, type ActionStatusIconVariant} from '../modules/action-status-icon.ts';
import type {ActionsStatus} from '../modules/gitea-actions.ts';
const props = withDefaults(defineProps<{
status: 'success' | 'skipped' | 'waiting' | 'blocked' | 'running' | 'failure' | 'cancelled' | 'cancelling' | 'unknown',
status: ActionsStatus,
size?: number,
className?: string,
localeStatus?: string,
+1 -1
View File
@@ -92,7 +92,7 @@ function isJobCollapsed(jobID: number) {
}
const visibleJobListItems = computed<JobListItem[]>(() => {
const jobs = [...(run.value.jobs || [])].sort((a, b) => a.id - b.id);
const jobs = (run.value.jobs || []).filter((job) => job.status !== 'pending' || job.id === props.jobId).sort((a, b) => a.id - b.id);
const childrenByParent = buildJobsByParentJobID(jobs);
const result: JobListItem[] = [];
@@ -160,7 +160,7 @@ test('reusable callers with identical dependency signature are kept as separate
{id: 1, link: '', jobId: 'prepare', name: 'prepare', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '30s'},
{id: 2, link: '', jobId: 'local_caller', name: 'local caller', status: 'running', canRerun: false, isReusableCaller: true, parentJobID: 0, duration: '5m', needs: ['prepare'], callUses: './.gitea/workflows/lib.yml'},
{id: 3, link: '', jobId: 'cross_caller', name: 'cross-repo caller', status: 'waiting', canRerun: false, isReusableCaller: true, parentJobID: 0, duration: '0s', needs: ['prepare'], callUses: 'user2/lib/.gitea/workflows/ext.yml@main'},
{id: 4, link: '', jobId: 'final', name: 'final', status: 'blocked', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '0s', needs: ['local_caller', 'cross_caller']},
{id: 4, link: '', jobId: 'final', name: 'final', status: 'pending', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '0s', needs: ['local_caller', 'cross_caller']},
];
const graph = createWorkflowGraphModel(jobs);
expect(graph.nodes.find((n) => n.type === 'group')).toBeUndefined();
+4 -3
View File
@@ -122,9 +122,10 @@ function compareStatusWorstFirst(a: ActionsStatus, b: ActionsStatus): number {
if (s === 'running') return 2;
if (s === 'waiting') return 3;
if (s === 'blocked') return 4;
if (s === 'success') return 5;
if (s === 'skipped') return 6;
return 7;
if (s === 'pending') return 5;
if (s === 'success') return 6;
if (s === 'skipped') return 7;
return 8;
};
return rank(a) - rank(b);
}
+14 -5
View File
@@ -228,11 +228,13 @@ const splitRoutedEdges = computed(() => {
const nodesWithIncomingEdge = computed(() => new Set(graphModel.value.adjacency.incomingByNodeId.keys()));
const nodesWithOutgoingEdge = computed(() => new Set(graphModel.value.adjacency.outgoingByNodeId.keys()));
function isJobLinked(job: ActionsJob) {
return !job.isReusableCaller && job.status !== 'pending'; // callers have no detail page, pending jobs have nothing to show yet
}
function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
const target = 'jobs' in job ? job.jobs[0]! : job;
// Reusable callers have no per-job detail page; clicking them is a no-op so the graph
// doesn't lead users to a dead destination.
if (target.isReusableCaller) return;
if (!isJobLinked(target)) return;
const link = `${props.runLink}/jobs/${target.id}`;
if (event.ctrlKey || event.metaKey) {
window.open(link, '_blank');
@@ -342,6 +344,7 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
v-for="ch in job.jobs"
:key="ch.id"
class="graph-list-row"
:class="{ 'unlinked-node': !isJobLinked(ch) }"
@mouseenter="handleNodeMouseEnter(job.id)"
@click.stop="onNodeClick(ch, $event)"
>
@@ -373,6 +376,7 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
v-for="ch in job.jobs"
:key="ch.id"
class="graph-list-row"
:class="{ 'unlinked-node': !isJobLinked(ch) }"
@mouseenter="handleNodeMouseEnter(job.id)"
@click="onNodeClick(ch, $event)"
>
@@ -391,7 +395,7 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
<g
v-else
class="job-node-group"
:class="{ 'related-node': isNodeHighlighted(job.id), 'caller-node': job.jobs[0]!.isReusableCaller }"
:class="{ 'related-node': isNodeHighlighted(job.id), 'unlinked-node': !isJobLinked(job.jobs[0]!) }"
@click="onNodeClick(job, $event)"
@mouseenter="handleNodeMouseEnter(job.id)"
@mouseleave="handleNodeMouseLeave"
@@ -523,10 +527,15 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
transition: opacity 0.15s ease;
}
.job-node-group.caller-node {
.job-node-group.unlinked-node,
.graph-list-row.unlinked-node {
cursor: default;
}
.graph-list-row.unlinked-node:hover {
background: none;
}
.job-node-group:hover .job-rect,
.job-node-group.related-node .job-rect {
stroke: var(--color-primary);
+1
View File
@@ -89,6 +89,7 @@ function initRepositoryActionsView() {
cancelled: el.getAttribute('data-locale-status-cancelled'),
skipped: el.getAttribute('data-locale-status-skipped'),
blocked: el.getAttribute('data-locale-status-blocked'),
pending: el.getAttribute('data-locale-status-pending'),
},
logsAlwaysAutoScroll: el.getAttribute('data-locale-logs-always-auto-scroll'),
logsAlwaysExpandRunning: el.getAttribute('data-locale-logs-always-expand-running'),
+2
View File
@@ -19,6 +19,8 @@ export function getActionStatusIcon(status: ActionsStatus, iconVariant: ActionSt
case 'cancelled':
return {name: 'octicon-stop', colorClass: 'tw-text-text-light'};
case 'waiting':
return {name: 'octicon-dot-fill', colorClass: 'tw-text-yellow'};
case 'pending':
return {name: 'octicon-circle', colorClass: 'tw-text-text-light'};
case 'blocked':
return {name: 'octicon-blocked', colorClass: 'tw-text-yellow'};
+1 -1
View File
@@ -1,5 +1,5 @@
// see "models/actions/status.go", if it needs to be used somewhere else, move it to a shared file like "types/actions.ts"
export type ActionsStatus = 'unknown' | 'waiting' | 'running' | 'cancelling' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked';
export type ActionsStatus = 'unknown' | 'waiting' | 'running' | 'cancelling' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked' | 'pending';
export type ActionsArtifactStatus = 'expired' | 'completed';
export type ActionsRun = {