feat(obs-integration): add package's build status view (#284)

* feat(obs-integration): add package's build status view

* i18n(obs-integration): new translation for build status

* chore(obs-integration): update plugin to v1.1.0

* docs(obs-integration): update README

* i18n(obs-integration): fix duplicates and missing comma in translation

---------

Co-authored-by: neyfua <ogkhang.05@gmail.com>
This commit is contained in:
Lê Khang
2026-08-06 10:39:06 -04:00
committed by GitHub
co-authored by neyfua
parent cd313deb97
commit 8f52ef14cf
4 changed files with 135 additions and 9 deletions
+6 -2
View File
@@ -64,8 +64,8 @@ its actions:
- **Service Remote Run** — runs `osc service rr` on the OBS server for the
package on the remote project. No local code executes; the request triggers
server-side service runs, and a confirmation is not required.
- **Rebuild package** — pick an architecture (or **All**) and trigger
`osc rebuild`. Confirmation is required before it runs.
- **Rebuild package** — pick a repository and architecture (or **All**) and trigger `osc rebuild`. Confirmation is required before it runs.
- **Build Status** — Shows real-time build results for the package across all repositories and architectures. Click **Refresh status** to pull latest results.
- **Edit package meta** — `osc meta pkg -e` in a terminal.
- **Remove package** — deletes the checkout from disk only (never touches the
OBS project); confirm before it runs. If it was the last package in the
@@ -93,3 +93,7 @@ operation opens a terminal only when `osc status` reports pending changes;
otherwise, it runs asynchronously in the panel. Confirmation prompts precede
destructive and side-effecting operations: Rebuild package, Remove package,
and Service Run All.
Build status data is fetched from `osc results` and cached for the session. The
status display shares the same underlying `osc results` call used for rebuild
architecture selection, so enabling a package view pulls both in one query.
+110 -2
View File
@@ -21,6 +21,7 @@ local rebuildRepoIndex = 0
local rebuildConfirm = false
local runAllConfirm = false
local repoMap = {}
local statusMap = {}
local savedRebuildRepo = nil
local savedRebuildArch = nil
local rebuildLoaded = false
@@ -275,17 +276,25 @@ loadPackageFiles = function(pkgDir, pkg)
end, 2000)
end
local function loadRebuildArches(project, pkg)
local function loadRebuildArches(project, pkg)
rebuildLoaded = false
rebuildRepos = {}
repoMap = {}
statusMap = {}
noctalia.runAsync("osc results " .. shellQuote(project) .. " " .. shellQuote(pkg), function(result)
local map = {}
local statuses = {}
if result.exitCode == 0 then
for line in tostring(result.stdout):gmatch("[^\n]+") do
line = noctalia.string.trim(line)
local repo, arch, _, status = line:match("^(%S+)%s+(%S+)%s+(%S+)%s+(%S+)")
if status then
status = status:gsub("%*$", "")
end
if repo ~= nil and arch ~= nil and status ~= "disabled" then
if not map[repo] then
map[repo] = {}
statuses[repo] = {}
end
local found = false
for _, a in ipairs(map[repo]) do
@@ -297,6 +306,7 @@ local function loadRebuildArches(project, pkg)
if not found then
table.insert(map[repo], arch)
end
statuses[repo][arch] = status
end
end
end
@@ -309,6 +319,7 @@ local function loadRebuildArches(project, pkg)
table.sort(map[repo])
end
repoMap = map
statusMap = statuses
rebuildRepos = repos
local repoIndex = 0
if savedRebuildRepo then
@@ -336,6 +347,50 @@ local function loadRebuildArches(project, pkg)
end, 15000)
end
local STATUS_STYLE = {
succeeded = { glyph = "check", color = "secondary", labelKey = "status_succeeded" },
failure = { glyph = "x", color = "error", labelKey = "status_failed" },
broken = { glyph = "alert-circle", color = "error", labelKey = "status_failed" },
unresolvable = { glyph = "alert-circle", color = "error", labelKey = "status_unresolvable" },
building = { glyph = "loader", color = "primary", labelKey = "status_building" },
dispatching = { glyph = "loader", color = "primary", labelKey = "status_building" },
scheduled = { glyph = "clock", color = "on_surface_variant", labelKey = "status_scheduled" },
signing = { glyph = "lock", color = "warning", labelKey = "status_signing" },
blocked = { glyph = "clock", color = "on_surface_variant", labelKey = "status_blocked" },
excluded = { glyph = "ban", color = "on_surface_variant", labelKey = "status_excluded" },
unbuildable = { glyph = "ban", color = "on_surface_variant", labelKey = "status_unbuildable" },
finished = { glyph = "check", color = "secondary", labelKey = "status_succeeded" },
unknown = { glyph = "help-circle", color = "on_surface_variant", labelKey = "status_unknown" },
}
local STATUS_GROUPS = {
["succeeded"] = "succeeded",
["success"] = "succeeded",
["finished"] = "succeeded",
["failed"] = "failure",
["broken"] = "failure",
["unresolvable"] = "unresolvable",
["building"] = "building",
["dispatching"] = "building",
["scheduled"] = "scheduled",
["signing"] = "signing",
["blocked"] = "blocked",
["excluded"] = "excluded",
["unbuildable"] = "unbuildable",
["unknown"] = "unknown",
["disabled"] = "disabled",
}
local function statusPresentation(status)
status = status or "unknown"
local group = STATUS_GROUPS[status] or status
local base = STATUS_STYLE[group]
if base then
return base.glyph, base.color, noctalia.tr("panel." .. base.labelKey), group
end
return "help-circle", "on_surface_variant", status, "unknown"
end
local function removeFileLocal(path, pkgDir, pkg)
noctalia.runAsync("rm " .. shellQuote(path), function(result)
if result.exitCode == 0 then
@@ -831,6 +886,7 @@ function render()
rebuildRepos = {}
rebuildRepoIndex = 0
repoMap = {}
statusMap = {}
savedRebuildRepo = nil
savedRebuildArch = nil
rebuildLoaded = false
@@ -1262,7 +1318,7 @@ function render()
}),
ui.button({
key = "rebuild-btn",
glyph = rebuildConfirm and "check" or "refresh",
glyph = rebuildConfirm and "check" or "analyze-filled",
tooltip = rebuildConfirm and noctalia.tr("panel.confirm_rebuild") or noctalia.tr("panel.rebuild"),
variant = rebuildConfirm and "secondary" or "default",
onClick = function()
@@ -1297,6 +1353,58 @@ function render()
end
end
table.insert(body, ui.separator({ thickness = 1, spacing = 6 }))
local statusChildren = {
ui.row({ align = "center", gap = 8 }, {
ui.glyph({ name = "activity", color = "primary", size = 16 }),
ui.label({ text = noctalia.tr("panel.build_status"), fontWeight = "bold", fontSize = 15, flexGrow = 1 }),
ui.button({
glyph = "refresh",
tooltip = noctalia.tr("panel.status_refresh"),
variant = "ghost",
controlSize = "sm",
onClick = function()
loadRebuildArches(selectedProject, selectedPackage)
end,
}),
}),
}
if not rebuildLoaded then
table.insert(statusChildren, ui.row({ align = "center", gap = 8, paddingV = 2 }, {
ui.glyph({ name = "loader", color = "primary", size = 14 }),
ui.label({ text = noctalia.tr("panel.status_loading"), color = "on_surface_variant" }),
}))
else
local hadResults = false
for _, repo in ipairs(rebuildRepos) do
local archs = repoMap[repo] or {}
if #archs > 0 then
hadResults = true
table.insert(
statusChildren,
ui.label({ text = repo, fontWeight = "bold", fontSize = 14, color = "on_surface" })
)
for _, arch in ipairs(archs) do
local status = statusMap[repo] and statusMap[repo][arch] or nil
local glyph, color, label = statusPresentation(status)
table.insert(
statusChildren,
ui.row({ align = "center", gap = 8, paddingL = 12 }, {
ui.glyph({ name = glyph, color = color, size = 14 }),
ui.label({ text = arch, fontSize = 13, color = "on_surface", flexGrow = 1 }),
ui.label({ text = label, fontSize = 13, color = color }),
})
)
end
end
end
if not hadResults then
table.insert(statusChildren, ui.label({ text = noctalia.tr("panel.no_builds"), color = "on_surface_variant" }))
end
end
table.insert(body, ui.column({ gap = 4 }, statusChildren))
elseif selectedProject then
table.insert(
body,
+1 -1
View File
@@ -1,6 +1,6 @@
id = "neyfua/obs-integration"
name = "OBS Integration"
version = "1.0.0"
version = "1.1.0"
plugin_api = 9
author = "neyfua"
license = "MIT"
+18 -4
View File
@@ -26,10 +26,6 @@
"no_files": "No files found.",
"no_packages": "No packages found.",
"no_projects": "No projects found.",
"osc_missing": "'osc' not found",
"osc_missing_hint": "Install the 'osc' package to start using this plugin.",
"osc_not_configured": "OBS account not configured",
"osc_not_configured_hint": "Create ~/.config/osc/oscrc before browsing projects.",
"rebuild": "Rebuild package",
"rebuilding_pkg": "Rebuilding package...",
"reload_projects": "Reload projects",
@@ -47,6 +43,24 @@
"service_remote_run": "Service Remote Run",
"service_run": "Service Manual Run",
"service_run_all": "Service Run All",
"osc_missing": "'osc' not found",
"osc_missing_hint": "Install the 'osc' package to start using this plugin.",
"osc_not_configured": "OBS account not configured",
"osc_not_configured_hint": "Create ~/.config/osc/oscrc before browsing projects.",
"build_status": "Build Status",
"status_refresh": "Refresh status",
"status_loading": "Loading status...",
"no_builds": "No build results.",
"status_succeeded": "Succeeded",
"status_failed": "Failed",
"status_unresolvable": "Unresolvable",
"status_building": "Building",
"status_scheduled": "Scheduled",
"status_signing": "Signing",
"status_blocked": "Blocked",
"status_excluded": "Excluded",
"status_unbuildable": "Unbuildable",
"status_unknown": "Unknown",
"update_changes": "Update {name}.changes",
"update_dir": "Update directory",
"updating_dir": "Updating directory..."