mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-08 22:13:26 +09:00
Compare commits
2
Commits
69f0a10364
...
a2166293f7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2166293f7 | ||
|
|
396ec646f6 |
@@ -270,15 +270,31 @@ func (opts FindRunnerOptions) ToConds() builder.Cond {
|
||||
return cond
|
||||
}
|
||||
|
||||
// runnerStatusOrderExpr builds an ORDER BY fragment that ranks runners by their
|
||||
// computed status (see ActionRunner.Status): active (0), idle (1), offline (2).
|
||||
// The thresholds are evaluated against the current time, mirroring ToConds, so
|
||||
// sorting by status groups active and idle runners instead of interleaving them
|
||||
// by raw last_online.
|
||||
func runnerStatusOrderExpr() string {
|
||||
now := time.Now()
|
||||
offlineThreshold := now.Add(-RunnerOfflineTime).Unix()
|
||||
idleThreshold := now.Add(-RunnerIdleTime).Unix()
|
||||
return fmt.Sprintf("CASE WHEN last_online <= %d THEN 2 WHEN last_active <= %d THEN 1 ELSE 0 END", offlineThreshold, idleThreshold)
|
||||
}
|
||||
|
||||
func (opts FindRunnerOptions) ToOrders() string {
|
||||
// A unique tiebreaker (id) is appended so that runners sharing the same
|
||||
// last_online or name keep a deterministic order across paginated queries,
|
||||
// otherwise the same runner may appear on more than one page.
|
||||
// status, last_online or name keep a deterministic order across paginated
|
||||
// queries, otherwise the same runner may appear on more than one page.
|
||||
statusRank := runnerStatusOrderExpr()
|
||||
switch opts.Sort {
|
||||
case "online":
|
||||
return "last_online DESC, id ASC"
|
||||
// Rank by computed status first so idle runners are not interleaved with
|
||||
// active ones; disabled runners sink to the bottom of their status group
|
||||
// (is_disabled ASC), then last_online breaks ties within a group.
|
||||
return statusRank + " ASC, is_disabled ASC, last_online DESC, id ASC"
|
||||
case "offline":
|
||||
return "last_online ASC, id ASC"
|
||||
return statusRank + " DESC, is_disabled ASC, last_online ASC, id ASC"
|
||||
case "alphabetically":
|
||||
return "name ASC, id ASC"
|
||||
case "reversealphabetically":
|
||||
@@ -288,7 +304,7 @@ func (opts FindRunnerOptions) ToOrders() string {
|
||||
case "oldest":
|
||||
return "id ASC"
|
||||
}
|
||||
return "last_online DESC, id ASC"
|
||||
return statusRank + " ASC, is_disabled ASC, last_online DESC, id ASC"
|
||||
}
|
||||
|
||||
// GetRunnerByUUID returns a runner via uuid
|
||||
|
||||
@@ -4,16 +4,12 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestShouldPersistLastOnline(t *testing.T) {
|
||||
@@ -85,65 +81,3 @@ func TestShouldPersistLastActive(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindRunnerOptions_ToOrders_StableTiebreaker(t *testing.T) {
|
||||
// Sorts on a non-unique column must end with the unique id tiebreaker so
|
||||
// pagination is deterministic; without it, runners sharing the same
|
||||
// last_online or name can appear on more than one page. Sorts already on
|
||||
// the unique id need no tiebreaker.
|
||||
expected := map[string]string{
|
||||
"": "last_online DESC, id ASC",
|
||||
"online": "last_online DESC, id ASC",
|
||||
"offline": "last_online ASC, id ASC",
|
||||
"alphabetically": "name ASC, id ASC",
|
||||
"reversealphabetically": "name DESC, id ASC",
|
||||
"newest": "id DESC",
|
||||
"oldest": "id ASC",
|
||||
}
|
||||
for sort, want := range expected {
|
||||
assert.Equal(t, want, FindRunnerOptions{Sort: sort}.ToOrders(), "sort %q", sort)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindRunners_PaginationNoDuplicates(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
ctx := t.Context()
|
||||
|
||||
// Create several runners that all share the same last_online value so the
|
||||
// primary sort key (last_online) is tied for all of them.
|
||||
const ownerID = 1000
|
||||
const count = 6
|
||||
for i := range count {
|
||||
runner := &ActionRunner{
|
||||
Name: "paginated-runner",
|
||||
UUID: fmt.Sprintf("PAGINATE-TEST-0000-0000-00000000000%d", i),
|
||||
TokenHash: fmt.Sprintf("paginate-test-token-hash-%d", i),
|
||||
OwnerID: ownerID,
|
||||
RepoID: 0,
|
||||
LastOnline: 42,
|
||||
}
|
||||
require.NoError(t, db.Insert(ctx, runner))
|
||||
}
|
||||
|
||||
// Page through the runners and ensure every id is returned exactly once.
|
||||
seen := make(map[int64]int)
|
||||
const pageSize = 2
|
||||
for page := 1; ; page++ {
|
||||
runners, err := db.Find[ActionRunner](ctx, FindRunnerOptions{
|
||||
ListOptions: db.ListOptions{Page: page, PageSize: pageSize},
|
||||
OwnerID: ownerID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
if len(runners) == 0 {
|
||||
break
|
||||
}
|
||||
for _, r := range runners {
|
||||
seen[r.ID]++
|
||||
}
|
||||
}
|
||||
|
||||
assert.Len(t, seen, count, "each runner should be returned exactly once across all pages")
|
||||
for id, n := range seen {
|
||||
assert.Equal(t, 1, n, "runner %d appeared on %d pages", id, n)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,11 +85,15 @@ func (task *ActionTask) IsStopped() bool {
|
||||
return task.Stopped > 0
|
||||
}
|
||||
|
||||
func (task *ActionTask) GetRunLink() string {
|
||||
if task.Job == nil || task.Job.Run == nil {
|
||||
func (task *ActionTask) GetRunJobLink() string {
|
||||
// Run.Repo can be nil when the repository was deleted while task/run rows remain
|
||||
// (TaskList.LoadAttributes copies job.Repo into run.Repo, leaving it nil on a miss).
|
||||
// Run.Link() already returns "" in that case, so guard here to avoid emitting a
|
||||
// broken relative "/jobs/N" link from the Sprintf below.
|
||||
if task.Job == nil || task.Job.Run == nil || task.Job.Run.Repo == nil {
|
||||
return ""
|
||||
}
|
||||
return task.Job.Run.Link()
|
||||
return fmt.Sprintf("%s/jobs/%d", task.Job.Run.Link(), task.Job.ID)
|
||||
}
|
||||
|
||||
func (task *ActionTask) GetCommitLink() string {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
runnerv1 "gitea.dev/actions-proto-go/runner/v1"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/actions/jobparser"
|
||||
"gitea.dev/modules/timeutil"
|
||||
@@ -18,6 +19,21 @@ import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func TestActionTask_GetRunJobLink(t *testing.T) {
|
||||
repo := &repo_model.Repository{OwnerName: "org", Name: "consumer"}
|
||||
run := &ActionRun{ID: 10, Repo: repo}
|
||||
job := &ActionRunJob{ID: 42, Run: run}
|
||||
|
||||
// a task with a loaded job links to that specific job, not just the run
|
||||
task := &ActionTask{Job: job}
|
||||
assert.Equal(t, run.Link()+"/jobs/42", task.GetRunJobLink())
|
||||
|
||||
// missing job, run or repo yields an empty link instead of a broken URL
|
||||
assert.Empty(t, (&ActionTask{}).GetRunJobLink())
|
||||
assert.Empty(t, (&ActionTask{Job: &ActionRunJob{ID: 42}}).GetRunJobLink())
|
||||
assert.Empty(t, (&ActionTask{Job: &ActionRunJob{ID: 42, Run: &ActionRun{ID: 10}}}).GetRunJobLink())
|
||||
}
|
||||
|
||||
func TestMakeTaskStepDisplayName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -3191,7 +3191,6 @@
|
||||
"actions.runners.last_online": "Poslední čas online",
|
||||
"actions.runners.task_list": "Nedávné úlohy na tomto runneru",
|
||||
"actions.runners.task_list.no_tasks": "Zatím zde nejsou žádné úlohy.",
|
||||
"actions.runners.task_list.run": "Spustit",
|
||||
"actions.runners.task_list.repository": "Repozitář",
|
||||
"actions.runners.task_list.done_at": "Dokončeno v",
|
||||
"actions.runners.edit_runner": "Upravit Runner",
|
||||
|
||||
@@ -3740,7 +3740,6 @@
|
||||
"actions.runners.runner_title": "Runner",
|
||||
"actions.runners.task_list": "Letzte Aufgaben dieses Runners",
|
||||
"actions.runners.task_list.no_tasks": "Es gibt noch keine Aufgabe.",
|
||||
"actions.runners.task_list.run": "Ausführen",
|
||||
"actions.runners.task_list.status": "Status",
|
||||
"actions.runners.task_list.repository": "Repository",
|
||||
"actions.runners.task_list.commit": "Commit",
|
||||
|
||||
@@ -2906,7 +2906,6 @@
|
||||
"actions.runners.runner_title": "Εκτελεστής",
|
||||
"actions.runners.task_list": "Πρόσφατες εργασίες στον εκτελεστή",
|
||||
"actions.runners.task_list.no_tasks": "Δεν υπάρχει καμία εργασία ακόμα.",
|
||||
"actions.runners.task_list.run": "Εκτέλεση",
|
||||
"actions.runners.task_list.status": "Κατάσταση",
|
||||
"actions.runners.task_list.repository": "Αποθετήριο",
|
||||
"actions.runners.task_list.commit": "Υποβολή",
|
||||
|
||||
@@ -3741,7 +3741,7 @@
|
||||
"actions.runners.runner_title": "Runner",
|
||||
"actions.runners.task_list": "Recent tasks on this runner",
|
||||
"actions.runners.task_list.no_tasks": "There is no task yet.",
|
||||
"actions.runners.task_list.run": "Run",
|
||||
"actions.runners.task_list.job": "Job",
|
||||
"actions.runners.task_list.status": "Status",
|
||||
"actions.runners.task_list.repository": "Repository",
|
||||
"actions.runners.task_list.commit": "Commit",
|
||||
|
||||
@@ -2871,7 +2871,6 @@
|
||||
"actions.runners.runner_title": "Nodo",
|
||||
"actions.runners.task_list": "Tareas recientes en este nodo",
|
||||
"actions.runners.task_list.no_tasks": "Todavía no hay tarea.",
|
||||
"actions.runners.task_list.run": "Ejecutar",
|
||||
"actions.runners.task_list.status": "Estado",
|
||||
"actions.runners.task_list.repository": "Repositorio",
|
||||
"actions.runners.task_list.done_at": "Hecho en",
|
||||
|
||||
@@ -2193,7 +2193,6 @@
|
||||
"actions.runners.owner_type": "نوع",
|
||||
"actions.runners.description": "شرح",
|
||||
"actions.runners.labels": "برچسبها",
|
||||
"actions.runners.task_list.run": "اجرا",
|
||||
"actions.runners.task_list.repository": "مخزن",
|
||||
"actions.runners.task_list.commit": "کامیت",
|
||||
"actions.runners.status.active": "فعال",
|
||||
|
||||
@@ -1457,7 +1457,6 @@
|
||||
"actions.runners.owner_type": "Tyyppi",
|
||||
"actions.runners.description": "Kuvaus",
|
||||
"actions.runners.labels": "Tunnisteet",
|
||||
"actions.runners.task_list.run": "Suorita",
|
||||
"actions.runners.task_list.repository": "Repo",
|
||||
"actions.runners.version": "Versio",
|
||||
"actions.runs.branch": "Haara",
|
||||
|
||||
@@ -244,7 +244,7 @@
|
||||
"install.db_title": "Paramètres de la base de données",
|
||||
"install.db_type": "Type de base de données",
|
||||
"install.host": "Hôte",
|
||||
"install.user": "Nom d'utilisateur",
|
||||
"install.user": "Nom d’utilisateur",
|
||||
"install.password": "Mot de passe",
|
||||
"install.db_name": "Nom de base de données",
|
||||
"install.db_schema": "Schéma",
|
||||
@@ -1622,7 +1622,7 @@
|
||||
"repo.issues.lock.notice_2": "- Vous et les autres collaborateurs ayant accès à ce dépôt peuvent toujours laisser des commentaires que d’autres peuvent voir.",
|
||||
"repo.issues.lock.notice_3": "- Vous pouvez toujours déverrouiller ce ticket à l'avenir.",
|
||||
"repo.issues.unlock.notice_1": "- Tout le monde sera de nouveau en mesure de commenter ce ticket.",
|
||||
"repo.issues.unlock.notice_2": "- Vous pouvez toujours verrouiller ce ticket à l’avenir.",
|
||||
"repo.issues.unlock.notice_2": "- Vous pouvez toujours reverrouiller ce ticket plus tard.",
|
||||
"repo.issues.lock.reason": "Motif de verrouillage",
|
||||
"repo.issues.lock.title": "Verrouiller la conversation sur ce ticket.",
|
||||
"repo.issues.unlock.title": "Déverrouiller la conversation sur ce ticket.",
|
||||
@@ -1834,7 +1834,7 @@
|
||||
"repo.pulls.cannot_auto_merge_helper": "Fusionner manuellement pour résoudre les conflits.",
|
||||
"repo.pulls.num_conflicting_files_1": "%d fichier en conflit",
|
||||
"repo.pulls.num_conflicting_files_n": "%d fichiers en conflit",
|
||||
"repo.pulls.approve_count_1": "%d approuvé",
|
||||
"repo.pulls.approve_count_1": "%d approbation",
|
||||
"repo.pulls.approve_count_n": "%d approuvés",
|
||||
"repo.pulls.reject_count_1": "%d changement requis",
|
||||
"repo.pulls.reject_count_n": "%d changements requis",
|
||||
@@ -1846,10 +1846,10 @@
|
||||
"repo.pulls.no_merge_wip": "Cette demande d’ajout ne peut pas être fusionnée car elle est marquée en chantier.",
|
||||
"repo.pulls.no_merge_not_ready": "Cette demande d’ajout n’est pas prête à être fusionnée, vérifiez les évaluations et les signaux.",
|
||||
"repo.pulls.no_merge_access": "Vous n'êtes pas autorisé⋅e à fusionner cette demande d'ajout.",
|
||||
"repo.pulls.merge_pull_request": "Créer une révision de fusion",
|
||||
"repo.pulls.merge_pull_request": "Fusionner",
|
||||
"repo.pulls.rebase_merge_pull_request": "Rebaser puis rattraper",
|
||||
"repo.pulls.rebase_merge_commit_pull_request": "Rebaser puis créer une révision de fusion",
|
||||
"repo.pulls.squash_merge_pull_request": "Créer une révision de concaténation",
|
||||
"repo.pulls.rebase_merge_commit_pull_request": "Rebaser puis fusionner",
|
||||
"repo.pulls.squash_merge_pull_request": "Aplatir",
|
||||
"repo.pulls.fast_forward_only_merge_pull_request": "Avance rapide uniquement",
|
||||
"repo.pulls.merge_manually": "Fusionner manuellement",
|
||||
"repo.pulls.merge_commit_id": "L'ID de la révision de fusion",
|
||||
@@ -2977,7 +2977,7 @@
|
||||
"admin.dashboard.cleanup_hook_task_table": "Nettoyer la table hook_task",
|
||||
"admin.dashboard.cleanup_packages": "Nettoyer des paquets expirés",
|
||||
"admin.dashboard.cleanup_actions": "Nettoyer les reliquats des actions obsolètes",
|
||||
"admin.dashboard.server_uptime": "Uptime du serveur",
|
||||
"admin.dashboard.server_uptime": "En service depuis",
|
||||
"admin.dashboard.current_goroutine": "Goroutines actuelles",
|
||||
"admin.dashboard.current_memory_usage": "Utilisation Mémoire actuelle",
|
||||
"admin.dashboard.total_memory_allocated": "Mémoire totale allouée",
|
||||
@@ -2985,12 +2985,12 @@
|
||||
"admin.dashboard.pointer_lookup_times": "Nombre de Consultations Pointeur",
|
||||
"admin.dashboard.memory_allocate_times": "Allocations de mémoire",
|
||||
"admin.dashboard.memory_free_times": "Nombre de libérations de mémoire",
|
||||
"admin.dashboard.current_heap_usage": "Utilisation Tas (Heap)",
|
||||
"admin.dashboard.heap_memory_obtained": "Mémoire Tas (Heap) obtenue",
|
||||
"admin.dashboard.heap_memory_idle": "Mémoire Tas (Heap) au Repos",
|
||||
"admin.dashboard.heap_memory_in_use": "Utilisation Mémoire Tas (Heap)",
|
||||
"admin.dashboard.heap_memory_released": "Mémoire Tas (Heap) libérée",
|
||||
"admin.dashboard.heap_objects": "Objets Tas (Heap)",
|
||||
"admin.dashboard.current_heap_usage": "Pression sur le Tas",
|
||||
"admin.dashboard.heap_memory_obtained": "Quantité de Tas réservé",
|
||||
"admin.dashboard.heap_memory_idle": "Quantité de Tas au repos",
|
||||
"admin.dashboard.heap_memory_in_use": "Quantité de Tas utilisé",
|
||||
"admin.dashboard.heap_memory_released": "Quantité de Tas rendu",
|
||||
"admin.dashboard.heap_objects": "Objets du Tas",
|
||||
"admin.dashboard.bootstrap_stack_usage": "Utilisation Pile Bootstrap",
|
||||
"admin.dashboard.stack_memory_obtained": "Mémoire Pile obtenue",
|
||||
"admin.dashboard.mspan_structures_usage": "Utilisation des Structures MSpan",
|
||||
@@ -3740,7 +3740,6 @@
|
||||
"actions.runners.runner_title": "Opérateur",
|
||||
"actions.runners.task_list": "Tâches récentes de cet opérateur",
|
||||
"actions.runners.task_list.no_tasks": "Il n'y a pas de tâche ici.",
|
||||
"actions.runners.task_list.run": "Exécution",
|
||||
"actions.runners.task_list.status": "Statut",
|
||||
"actions.runners.task_list.repository": "Dépôt",
|
||||
"actions.runners.task_list.commit": "Révision",
|
||||
|
||||
@@ -3741,7 +3741,6 @@
|
||||
"actions.runners.runner_title": "Reathaí",
|
||||
"actions.runners.task_list": "Tascanna le déanaí ar an reathaí seo",
|
||||
"actions.runners.task_list.no_tasks": "Níl aon tasc ann fós.",
|
||||
"actions.runners.task_list.run": "Rith",
|
||||
"actions.runners.task_list.status": "Stádas",
|
||||
"actions.runners.task_list.repository": "Stóras",
|
||||
"actions.runners.task_list.commit": "Tiomantas",
|
||||
|
||||
@@ -1365,7 +1365,6 @@
|
||||
"actions.runners.owner_type": "Típus",
|
||||
"actions.runners.description": "Leírás",
|
||||
"actions.runners.labels": "Címkék",
|
||||
"actions.runners.task_list.run": "Futtatás",
|
||||
"actions.runners.task_list.repository": "Tároló",
|
||||
"actions.runners.status.active": "Aktív",
|
||||
"actions.runners.version": "Verzió",
|
||||
|
||||
@@ -1159,7 +1159,6 @@
|
||||
"actions.runners.name": "Nama",
|
||||
"actions.runners.owner_type": "Jenis",
|
||||
"actions.runners.description": "Deskripsi",
|
||||
"actions.runners.task_list.run": "Lari",
|
||||
"actions.runners.task_list.repository": "Repositori",
|
||||
"actions.runners.task_list.commit": "Memperbuat",
|
||||
"actions.runners.status.unspecified": "Tidak diketahui",
|
||||
|
||||
@@ -1104,7 +1104,6 @@
|
||||
"actions.runners.owner_type": "Tegund",
|
||||
"actions.runners.description": "Lýsing",
|
||||
"actions.runners.labels": "Lýsingar",
|
||||
"actions.runners.task_list.run": "Keyra",
|
||||
"actions.runners.task_list.repository": "Hugbúnaðarsafn",
|
||||
"actions.runners.task_list.commit": "Framlag",
|
||||
"actions.runners.status.active": "Virkt",
|
||||
|
||||
@@ -2346,7 +2346,6 @@
|
||||
"actions.runners.owner_type": "Tipo",
|
||||
"actions.runners.description": "Descrizione",
|
||||
"actions.runners.labels": "Etichette",
|
||||
"actions.runners.task_list.run": "Esegui",
|
||||
"actions.runners.status.active": "Attivo",
|
||||
"actions.runners.version": "Versione",
|
||||
"actions.runs.branch": "Ramo",
|
||||
|
||||
@@ -3740,7 +3740,6 @@
|
||||
"actions.runners.runner_title": "ランナー",
|
||||
"actions.runners.task_list": "このランナーの最近のタスク",
|
||||
"actions.runners.task_list.no_tasks": "タスクはまだありません。",
|
||||
"actions.runners.task_list.run": "実行",
|
||||
"actions.runners.task_list.status": "ステータス",
|
||||
"actions.runners.task_list.repository": "リポジトリ",
|
||||
"actions.runners.task_list.commit": "コミット",
|
||||
|
||||
@@ -3731,7 +3731,6 @@
|
||||
"actions.runners.runner_title": "러너",
|
||||
"actions.runners.task_list": "이 러너의 최근 작업",
|
||||
"actions.runners.task_list.no_tasks": "아직 작업이 없습니다.",
|
||||
"actions.runners.task_list.run": "실행",
|
||||
"actions.runners.task_list.status": "상태",
|
||||
"actions.runners.task_list.repository": "리포지토리",
|
||||
"actions.runners.task_list.commit": "커밋",
|
||||
|
||||
@@ -2946,7 +2946,6 @@
|
||||
"actions.runners.runner_title": "Izpildītājs",
|
||||
"actions.runners.task_list": "Pēdējās darbības, kas izpildītas",
|
||||
"actions.runners.task_list.no_tasks": "Vēl nav uzdevumu.",
|
||||
"actions.runners.task_list.run": "Izpildīt",
|
||||
"actions.runners.task_list.status": "Statuss",
|
||||
"actions.runners.task_list.repository": "Repozitorijs",
|
||||
"actions.runners.task_list.commit": "Revīzija",
|
||||
|
||||
@@ -2064,7 +2064,6 @@
|
||||
"secrets.creation.description": "Omschrijving",
|
||||
"actions.runners.name": "Naam",
|
||||
"actions.runners.description": "Omschrijving",
|
||||
"actions.runners.task_list.run": "Uitvoeren",
|
||||
"actions.runners.task_list.repository": "Opslagplaats",
|
||||
"actions.runners.status.active": "Actief",
|
||||
"actions.runners.version": "Versie",
|
||||
|
||||
@@ -2079,7 +2079,6 @@
|
||||
"actions.runners.owner_type": "Typ",
|
||||
"actions.runners.description": "Opis",
|
||||
"actions.runners.labels": "Etykiety",
|
||||
"actions.runners.task_list.run": "Uruchom",
|
||||
"actions.runners.task_list.repository": "Repozytorium",
|
||||
"actions.runners.status.active": "Aktywne",
|
||||
"actions.runners.version": "Wersja",
|
||||
|
||||
@@ -3185,7 +3185,6 @@
|
||||
"actions.runners.last_online": "Última Vez Online",
|
||||
"actions.runners.task_list": "Tarefas recentes neste runner",
|
||||
"actions.runners.task_list.no_tasks": "Ainda não há nenhuma tarefa.",
|
||||
"actions.runners.task_list.run": "Executar",
|
||||
"actions.runners.task_list.repository": "Repositório",
|
||||
"actions.runners.task_list.done_at": "Feito em",
|
||||
"actions.runners.edit_runner": "Editar Runner",
|
||||
|
||||
@@ -3741,7 +3741,6 @@
|
||||
"actions.runners.runner_title": "Executor",
|
||||
"actions.runners.task_list": "Tarefas recentes deste executor",
|
||||
"actions.runners.task_list.no_tasks": "Ainda não há tarefas.",
|
||||
"actions.runners.task_list.run": "Executar",
|
||||
"actions.runners.task_list.status": "Estado",
|
||||
"actions.runners.task_list.repository": "Repositório",
|
||||
"actions.runners.task_list.commit": "Cometimento",
|
||||
|
||||
@@ -2892,7 +2892,6 @@
|
||||
"actions.runners.runner_title": "Раннер",
|
||||
"actions.runners.task_list": "Недавние задания на раннере",
|
||||
"actions.runners.task_list.no_tasks": "Задания пока нет.",
|
||||
"actions.runners.task_list.run": "Запуск",
|
||||
"actions.runners.task_list.status": "Статус",
|
||||
"actions.runners.task_list.repository": "Репозиторий",
|
||||
"actions.runners.task_list.commit": "коммит",
|
||||
|
||||
@@ -2154,7 +2154,6 @@
|
||||
"actions.runners.owner_type": "වර්ගය",
|
||||
"actions.runners.description": "සවිස්තරය",
|
||||
"actions.runners.labels": "ලේබල",
|
||||
"actions.runners.task_list.run": "ධාවනය",
|
||||
"actions.runners.task_list.repository": "කෝෂ්ඨය",
|
||||
"actions.runners.task_list.commit": "කැප",
|
||||
"actions.runners.status.active": "ක්රියාකාරී",
|
||||
|
||||
@@ -1717,7 +1717,6 @@
|
||||
"actions.runners.owner_type": "Typ",
|
||||
"actions.runners.description": "Beskrivning",
|
||||
"actions.runners.labels": "Etiketter",
|
||||
"actions.runners.task_list.run": "Kör",
|
||||
"actions.runners.task_list.repository": "Utvecklingskatalog",
|
||||
"actions.runners.status.active": "Aktiv",
|
||||
"actions.runs.summary": "Översikt",
|
||||
|
||||
@@ -3607,7 +3607,6 @@
|
||||
"actions.runners.runner_title": "Çalıştırıcı",
|
||||
"actions.runners.task_list": "Bu çalıştırıcıdaki son görevler",
|
||||
"actions.runners.task_list.no_tasks": "Henüz bir görev yok.",
|
||||
"actions.runners.task_list.run": "Çalıştır",
|
||||
"actions.runners.task_list.status": "Durum",
|
||||
"actions.runners.task_list.repository": "Depo",
|
||||
"actions.runners.task_list.commit": "İşle",
|
||||
|
||||
@@ -3072,7 +3072,6 @@
|
||||
"actions.runners.labels": "Мітки",
|
||||
"actions.runners.last_online": "Останній раз онлайн",
|
||||
"actions.runners.task_list.no_tasks": "Наразі завдань немає.",
|
||||
"actions.runners.task_list.run": "Запустити",
|
||||
"actions.runners.task_list.status": "Статус",
|
||||
"actions.runners.task_list.repository": "Репозиторій",
|
||||
"actions.runners.task_list.commit": "Коміт",
|
||||
|
||||
@@ -3740,7 +3740,6 @@
|
||||
"actions.runners.runner_title": "运行器",
|
||||
"actions.runners.task_list": "最近在此运行器上的任务",
|
||||
"actions.runners.task_list.no_tasks": "目前还没有任务。",
|
||||
"actions.runners.task_list.run": "执行",
|
||||
"actions.runners.task_list.status": "状态",
|
||||
"actions.runners.task_list.repository": "仓库",
|
||||
"actions.runners.task_list.commit": "提交",
|
||||
|
||||
@@ -3182,7 +3182,6 @@
|
||||
"actions.runners.last_online": "最後上線時間",
|
||||
"actions.runners.task_list": "最近在此 Runner 上的任務",
|
||||
"actions.runners.task_list.no_tasks": "目前還沒有任務。",
|
||||
"actions.runners.task_list.run": "執行",
|
||||
"actions.runners.task_list.status": "狀態",
|
||||
"actions.runners.task_list.repository": "儲存庫",
|
||||
"actions.runners.task_list.commit": "提交",
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<table class="ui very basic table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.run"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.job"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.status"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.repository"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.commit"}}</th>
|
||||
@@ -75,7 +75,7 @@
|
||||
<tbody>
|
||||
{{range .Tasks}}
|
||||
<tr>
|
||||
<td><a href="{{.GetRunLink}}" target="_blank">{{.ID}}</a></td>
|
||||
<td>{{if .Job}}<a href="{{.GetRunJobLink}}" target="_blank">{{.Job.ID}}</a>{{else}}{{.ID}}{{end}}</td>
|
||||
<td><span class="ui label task-status-{{.Status.String}}">{{.Status.LocaleString ctx.Locale}}</span></td>
|
||||
<td><a href="{{.GetRepoLink}}" target="_blank">{{.GetRepoName}}</a></td>
|
||||
<td>
|
||||
|
||||
@@ -62,6 +62,10 @@
|
||||
{{if .AllowBulkActions}}
|
||||
<th class="tw-w-8"><div class="ui checkbox tw-flex"><input type="checkbox" class="runner-bulk-select-all" aria-label="{{ctx.Locale.Tr "admin.notices.select_all"}}"></div></th>
|
||||
{{end}}
|
||||
<th data-sortt-asc="alphabetically" data-sortt-desc="reversealphabetically">
|
||||
{{ctx.Locale.Tr "actions.runners.name"}}
|
||||
{{SortArrow "alphabetically" "reversealphabetically" .SortType false}}
|
||||
</th>
|
||||
<th data-sortt-asc="online" data-sortt-desc="offline">
|
||||
{{ctx.Locale.Tr "actions.runners.status"}}
|
||||
{{SortArrow "online" "offline" .SortType false}}
|
||||
@@ -70,10 +74,6 @@
|
||||
{{ctx.Locale.Tr "actions.runners.id"}}
|
||||
{{SortArrow "oldest" "newest" .SortType false}}
|
||||
</th>
|
||||
<th data-sortt-asc="alphabetically" data-sortt-desc="reversealphabetically">
|
||||
{{ctx.Locale.Tr "actions.runners.name"}}
|
||||
{{SortArrow "alphabetically" "reversealphabetically" .SortType false}}
|
||||
</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.version"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.owner_type"}}</th>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.labels"}}</th>
|
||||
@@ -87,12 +87,12 @@
|
||||
{{if $.AllowBulkActions}}
|
||||
<td><div class="ui checkbox tw-flex"><input type="checkbox" class="runner-bulk-select" data-runner-id="{{.ID}}" aria-label="{{ctx.Locale.Tr "repo.issues.action_check"}}: {{.Name}}"></div></td>
|
||||
{{end}}
|
||||
<td><p data-tooltip-content="{{.Description}}">{{.Name}}</p></td>
|
||||
<td>
|
||||
<span class="ui label {{if .IsOnline}}green{{end}}">{{.StatusLocaleName ctx.Locale}}</span>
|
||||
{{if .IsDisabled}}<span class="ui grey label">{{ctx.Locale.Tr "actions.runners.disabled"}}</span>{{end}}
|
||||
<span class="ui label {{if eq .StatusName "active"}}green{{else if eq .StatusName "idle"}}yellow{{else if eq .StatusName "offline"}}red{{end}}">{{.StatusLocaleName ctx.Locale}}</span>
|
||||
{{if .IsDisabled}}<span class="ui grey label">{{ctx.Locale.Tr "disabled"}}</span>{{end}}
|
||||
</td>
|
||||
<td>{{.ID}}</td>
|
||||
<td><p data-tooltip-content="{{.Description}}">{{.Name}}</p></td>
|
||||
<td>{{if .Version}}{{.Version}}{{else}}{{ctx.Locale.Tr "unknown"}}{{end}}</td>
|
||||
<td><span data-tooltip-content="{{.BelongsToOwnerName}}">{{.BelongsToOwnerType.LocaleString ctx.Locale}}</span></td>
|
||||
<td>
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestFindRunnersSortByStatus verifies that sorting by status ranks runners by
|
||||
// their computed status (active, then idle, then offline) instead of interleaving
|
||||
// idle runners with active ones by raw last_online, and that a disabled runner
|
||||
// sinks to the bottom of its status group rather than mixing with enabled runners.
|
||||
//
|
||||
// It lives in tests/integration rather than a unit test because the status rank is
|
||||
// a database-evaluated CASE expression; unit tests only run against SQLite, so the
|
||||
// expression must be exercised against MySQL/PostgreSQL/MSSQL in CI to catch dialect
|
||||
// differences.
|
||||
func TestFindRunnersSortByStatus(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
ctx := t.Context()
|
||||
|
||||
const ownerID = 1001
|
||||
now := time.Now()
|
||||
// An idle runner that went online most recently would sort before an active
|
||||
// runner when ordering by last_online alone; the status rank must override that.
|
||||
insert := func(name string, lastOnline, lastActive time.Time, disabled bool) {
|
||||
require.NoError(t, db.Insert(ctx, &actions_model.ActionRunner{
|
||||
Name: name,
|
||||
UUID: "STATUS-SORT-" + name,
|
||||
TokenHash: "status-sort-token-" + name,
|
||||
OwnerID: ownerID,
|
||||
LastOnline: timeutil.TimeStamp(lastOnline.Unix()),
|
||||
LastActive: timeutil.TimeStamp(lastActive.Unix()),
|
||||
IsDisabled: disabled,
|
||||
}))
|
||||
}
|
||||
// Each disabled runner has the most recent last_online within its status group,
|
||||
// so it would sort first in that group if the disabled flag were ignored; the
|
||||
// last_active value keeps it in the intended group (offline<idle<active).
|
||||
insert("active", now.Add(-8*time.Second), now.Add(-5*time.Second), false)
|
||||
insert("active-disabled", now, now.Add(-2*time.Second), true)
|
||||
insert("idle", now.Add(-30*time.Second), now.Add(-20*time.Second), false)
|
||||
insert("idle-disabled", now, now.Add(-20*time.Second), true)
|
||||
insert("offline", now.Add(-3*time.Minute), now.Add(-3*time.Minute), false)
|
||||
insert("offline-disabled", now.Add(-90*time.Second), now.Add(-90*time.Second), true)
|
||||
|
||||
names := func(runners []*actions_model.ActionRunner) []string {
|
||||
out := make([]string, len(runners))
|
||||
for i, r := range runners {
|
||||
out[i] = r.Name
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Active group first, then idle, then offline; within each group enabled before disabled.
|
||||
runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{OwnerID: ownerID, Sort: "online"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{
|
||||
"active", "active-disabled",
|
||||
"idle", "idle-disabled",
|
||||
"offline", "offline-disabled",
|
||||
}, names(runners))
|
||||
|
||||
// The descending status sort reverses the group order but still keeps disabled
|
||||
// runners at the bottom of their group.
|
||||
runners, err = db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{OwnerID: ownerID, Sort: "offline"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{
|
||||
"offline", "offline-disabled",
|
||||
"idle", "idle-disabled",
|
||||
"active", "active-disabled",
|
||||
}, names(runners))
|
||||
}
|
||||
|
||||
// TestFindRunnersPaginationNoDuplicates verifies that the unique id tiebreaker in
|
||||
// FindRunnerOptions.ToOrders keeps pagination deterministic when the primary sort
|
||||
// key (last_online) is tied for every runner. It is an integration test so the
|
||||
// ordering is validated against every supported database, not only SQLite.
|
||||
func TestFindRunnersPaginationNoDuplicates(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
ctx := t.Context()
|
||||
|
||||
// Create several runners that all share the same last_online value so the
|
||||
// primary sort key (last_online) is tied for all of them.
|
||||
const ownerID = 1000
|
||||
const count = 6
|
||||
for i := range count {
|
||||
require.NoError(t, db.Insert(ctx, &actions_model.ActionRunner{
|
||||
Name: "paginated-runner",
|
||||
UUID: fmt.Sprintf("PAGINATE-TEST-0000-0000-00000000000%d", i),
|
||||
TokenHash: fmt.Sprintf("paginate-test-token-hash-%d", i),
|
||||
OwnerID: ownerID,
|
||||
RepoID: 0,
|
||||
LastOnline: 42,
|
||||
}))
|
||||
}
|
||||
|
||||
// Page through the runners and ensure every id is returned exactly once.
|
||||
seen := make(map[int64]int)
|
||||
const pageSize = 2
|
||||
for page := 1; ; page++ {
|
||||
runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{
|
||||
ListOptions: db.ListOptions{Page: page, PageSize: pageSize},
|
||||
OwnerID: ownerID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
if len(runners) == 0 {
|
||||
break
|
||||
}
|
||||
for _, r := range runners {
|
||||
seen[r.ID]++
|
||||
}
|
||||
}
|
||||
|
||||
assert.Len(t, seen, count, "each runner should be returned exactly once across all pages")
|
||||
for id, n := range seen {
|
||||
assert.Equal(t, 1, n, "runner %d appeared on %d pages", id, n)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user