feat(actions): add build queue view (#38585)

Adds a read-only Actions job queue: running jobs first, then waiting
jobs in the order a runner picks them up. It is shown instance-wide in
the admin Actions section with owner, repository and status filters, and
per repository in the Actions tab. Both lists refresh in place.

Pending work is currently only visible per repository and newest-first,
so nothing shows what is queued, in which order, or what occupies the
runners. Reordering the queue will be proposed separately.

A migration adds indexes for the runner pickup query and
repository-scoped status lookups.

* Fix #34198

<img width="1345" height="451" alt="image"
src="https://github.com/user-attachments/assets/7d52ff76-81b4-44e8-b583-d7d89c9dffcd"
/>
<img width="1809" height="1134" alt="image"
src="https://github.com/user-attachments/assets/4d56c0cb-bae7-4ce2-8f3c-75163b2bc7f4"
/>

---------

Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-09-26 08:01:41 +00:00
1 parent 5a56e118e4
commit 3875db1974
30 files changed
+928 -95

No files matched your search

+15 -18
View File
@@ -3,8 +3,7 @@ import RepoActionView from '../components/RepoActionView.vue';
import {registerGlobalInitFunc} from '../modules/observer.ts';
import {html} from '../utils/html.ts';
import {GET} from '../modules/fetch.ts';
import {activePageTimerRefresh, createElementFromHTML, protectMorphElements, recoverMorphElements} from '../utils/dom.ts';
import {Idiomorph} from 'idiomorph';
import {activePageTimerRefresh, createElementFromHTML, morphElementWithProtection} from '../utils/dom.ts';
export function updateWorkflowBadgeFields(form: HTMLElement, branch: string): void {
const badgeURLParsed = new URL(form.getAttribute('data-badge-url')!);
@@ -31,6 +30,7 @@ export function initRepositoryActions() {
registerGlobalInitFunc('initWorkflowBadgeForm', initWorkflowBadgeForm);
initRepositoryActionsView();
registerGlobalInitFunc('initActionRunsList', initActionRunsList);
registerGlobalInitFunc('initActionJobQueueList', initActionJobQueueList);
}
function initRepositoryActionsView() {
@@ -115,22 +115,19 @@ function initActionRunsList(el: HTMLElement) {
interval: () => Number(el.getAttribute('data-action-runs-refresh-interval')),
async callback() {
const resp = await GET(el.getAttribute('data-action-runs-refresh-link')!);
if (!resp.ok || resp.status !== 200) return;
const newEl = createElementFromHTML(await resp.text());
for (const attr of newEl.attributes) el.setAttribute(attr.name, attr.value);
for (const newItem of newEl.querySelectorAll(':scope > .item')) {
const oldItem = el.querySelector(`#${newItem.id}`);
if (!oldItem) continue;
// If the end user is operating the row, then don't refresh its content.
// Otherwise, there will be more edge cases and inconsistencies, e.g.: dropdown still shows old items but the icon has changed.
if (oldItem.querySelector('.ui.dropdown.active')) continue;
const protectedElems = protectMorphElements(newItem);
Idiomorph.morph(oldItem, newItem, {morphStyle: 'outerHTML'});
recoverMorphElements(el.querySelector(`#${newItem.id}`)!, protectedElems);
}
if (!resp.ok) return;
morphElementWithProtection(el, createElementFromHTML(await resp.text()), {morphStyle: 'outerHTML'});
},
});
}
function initActionJobQueueList(el: HTMLElement) {
activePageTimerRefresh({
interval: () => Number(el.getAttribute('data-job-queue-refresh-interval')),
async callback() {
const resp = await GET(el.getAttribute('data-job-queue-refresh-link')!);
if (!resp.ok) return;
morphElementWithProtection(el, createElementFromHTML(await resp.text()), {morphStyle: 'outerHTML'});
},
});
}