.pragma library
// CI status vocabulary, shared by every view. Mirrors the Android
// client's StatusChip.kt (and the `gg` CLI's output.rs) so the widget,
// the phone and the terminal agree on what a colour means.
//
// Two names here aren't server statuses: `awaiting_approval` on a run
// (runStatus — the server says `queued`) and `superseded`, a job
// cancelled because a newer one in its concurrency group replaced it
// (jobStatus). Superseded is grey, not red: the newer run decides.
// The only endings. Anything else — waiting, awaiting_approval, or a
// status the server learns later — is still in progress, so polling
// keeps going rather than calling it done.
var FINISHED = ["success", "failure", "cancelled", "skipped", "timed_out", "superseded"]
// `conclusion` when the thing has finished, else its raw `status`.
function effective(status, conclusion) {
if (status === "completed" || status === "finished") return conclusion || status
return status || "unknown"
}
// What a run's circle says. A fork's run waiting for approval is
// `queued` on the server, but "queued" would promise a runner is
// coming — nothing happens until someone approves it.
function runStatus(run) {
if (!run) return "unknown"
if (run.awaiting_approval && (run.status === "queued" || run.status === "running")) return "awaiting_approval"
return effective(run.status, run.conclusion)
}
// `superseded` for a job a newer one replaced, else its effective status.
function jobStatus(job) {
if (!job) return "unknown"
if (job.superseded) return "superseded"
return effective(job.status, job.conclusion)
}
function isFinished(status) {
return FINISHED.indexOf(status) !== -1 || status === "completed" || status === "finished"
}
// Not finished. "unknown" is this widget's own word for "nothing known
// yet" (a repo that never ran CI), not a server status, so it's idle.
function isActive(status) {
return !!status && status !== "unknown" && !isFinished(status)
}
// Worth the fast poll: in flight, but not parked on a person. A run
// waiting for approval is still watched on every poll, just at the idle
// pace; once approved it's `queued` again and the fast poll picks it up.
function pollsFast(status) {
return isActive(status) && status !== "awaiting_approval"
}
// success | failure | running | queued | dim
function category(status) {
switch (status) {
case "success":
return "success"
case "failure":
case "cancelled":
case "timed_out":
return "failure"
case "running":
return "running"
case "queued":
case "pending":
case "blocked":
case "waiting":
case "awaiting_approval":
return "queued"
case "skipped":
case "superseded":
return "dim"
default:
// A status this widget doesn't know yet but that hasn't ended
// reads as pending, not as idle.
return isActive(status) ? "queued" : "dim"
}
}
function color(status, theme) {
switch (category(status)) {
case "success": return theme.positiveTextColor
case "failure": return theme.negativeTextColor
case "running": return theme.neutralTextColor
case "queued": return theme.neutralTextColor
default: return theme.disabledTextColor
}
}
function icon(status) {
switch (status) {
case "success": return "emblem-success"
case "failure":
case "timed_out": return "emblem-error"
case "cancelled":
case "superseded": return "dialog-cancel"
case "running": return "media-playback-start"
case "queued":
case "pending":
case "blocked":
case "waiting": return "chronometer"
case "awaiting_approval": return "flag"
case "skipped": return "go-next-skip"
default: return "question"
}
}
// A status as words: `awaiting_approval` reads "needs approval",
// `timed_out` "timed out".
function label(status) {
if (!status) return "unknown"
if (status === "awaiting_approval") return "needs approval"
return status.replace(/_/g, " ")
}
// What started a run, as a person would say it. Unknown events fall
// through as their raw name with underscores spaced.
function triggerLabel(trigger) {
switch (trigger) {
case undefined:
case null:
case "": return ""
case "pull_request": return "pull request"
case "pull_request_target": return "pull request (target)"
case "workflow_dispatch": return "manual"
case "schedule": return "scheduled"
case "create": return "branch/tag created"
case "delete": return "branch/tag deleted"
case "workflow_run": return "after a workflow"
default: return String(trigger).replace(/_/g, " ")
}
}
// Worst-first ordering for folding several repos into one circle: a
// run in flight is the most interesting thing, then a broken build.
var PRIORITY = { running: 5, queued: 4, failure: 3, success: 2, dim: 1 }
function aggregate(statuses) {
var best = null
var bestScore = 0
for (var i = 0; i < statuses.length; i++) {
var score = PRIORITY[category(statuses[i])] || 0
if (score > bestScore) {
best = statuses[i]
bestScore = score
}
}
return best || "unknown"
}
// ── formatting ────────────────────────────────────────────────────────
function shortSha(sha) {
return sha ? sha.substring(0, 7) : ""
}
function shortRef(ref) {
if (!ref) return ""
return ref.replace(/^refs\/(heads|tags)\//, "")
}
function relativeTime(iso) {
if (!iso) return ""
var then = new Date(iso).getTime()
if (isNaN(then)) return ""
var secs = Math.max(0, Math.round((Date.now() - then) / 1000))
if (secs < 60) return "just now"
var mins = Math.round(secs / 60)
if (mins < 60) return mins + "m ago"
var hours = Math.round(mins / 60)
if (hours < 24) return hours + "h ago"
var days = Math.round(hours / 24)
return days + "d ago"
}
// Elapsed between two timestamps; a missing end means "still going".
function duration(startIso, endIso) {
if (!startIso) return ""
var start = new Date(startIso).getTime()
var end = endIso ? new Date(endIso).getTime() : Date.now()
if (isNaN(start) || isNaN(end)) return ""
var secs = Math.max(0, Math.round((end - start) / 1000))
if (secs < 60) return secs + "s"
var mins = Math.floor(secs / 60)
if (mins < 60) return mins + "m " + (secs % 60) + "s"
return Math.floor(mins / 60) + "h " + (mins % 60) + "m"
}
function runTitle(run) {
return run.workflow_name || run.workflow_path || "workflow"
}
function runMeta(run) {
var parts = []
if (run.head_ref) parts.push(shortRef(run.head_ref))
if (run.head_sha) parts.push(shortSha(run.head_sha))
if (run.trigger) parts.push(triggerLabel(run.trigger))
if (run.fork) parts.push("from a fork")
if (run.triggered_by && run.triggered_by.handle) parts.push("by " + run.triggered_by.handle)
return parts.join(" · ")
}
// What to say under a job: the server's reason (why it waits, was
// skipped or cancelled, or why its matrix couldn't expand), or for a
// matrix placeholder with nothing to explain yet, what it is.
function jobReason(job) {
if (job.status_reason && String(job.status_reason).trim() !== "") return job.status_reason
if (job.placeholder && job.needs && job.needs.length > 0 && jobStatus(job) === "blocked") {
return "Matrix expands when " + job.needs.join(", ") + " finish"
}
return ""
}
// "Waiting for approval" notice for a fork's run nobody has approved.
function approvalNotice(run) {
return "This run comes from a fork. Its jobs start once someone with write access approves it"
+ (run.pull_request_number ? " on pull request #" + run.pull_request_number + "." : ".")
}
function jobLabel(job) {
var label = job.name || job.job_id || ("job " + job.id)
if (job.matrix && typeof job.matrix === "object") {
var values = []
for (var k in job.matrix) values.push(job.matrix[k])
if (values.length > 0 && label.indexOf("(") === -1) label += " (" + values.join(", ") + ")"
}
return label
}
neiam
/gitgud-plasmoid
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
8.0 KiB · text
History
010513e