thepopebot 1.2.46 → 1.2.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/chat/actions.js +1 -1
- package/lib/chat/components/swarm-page.js +71 -126
- package/lib/chat/components/swarm-page.jsx +92 -170
- package/lib/tools/github.js +17 -67
- package/package.json +1 -1
- package/templates/.github/workflows/upgrade-event-handler.yml +11 -6
- package/templates/docker-compose.yml +1 -0
package/lib/chat/actions.js
CHANGED
|
@@ -245,7 +245,7 @@ export async function getSwarmStatus(page = 1) {
|
|
|
245
245
|
return await fetchStatus(page);
|
|
246
246
|
} catch (err) {
|
|
247
247
|
console.error('Failed to get swarm status:', err);
|
|
248
|
-
return { error: 'Failed to get swarm status',
|
|
248
|
+
return { error: 'Failed to get swarm status', runs: [], hasMore: false, counts: { running: 0, queued: 0 } };
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
251
|
|
|
@@ -29,7 +29,7 @@ function LoadingSkeleton() {
|
|
|
29
29
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
|
|
30
30
|
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-3", children: [...Array(2)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-20 animate-pulse rounded-md bg-border/50" }, i)) }),
|
|
31
31
|
/* @__PURE__ */ jsx("div", { className: "h-8 w-32 animate-pulse rounded-md bg-border/50" }),
|
|
32
|
-
[...Array(3)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-
|
|
32
|
+
[...Array(3)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-14 animate-pulse rounded-md bg-border/50" }, i))
|
|
33
33
|
] });
|
|
34
34
|
}
|
|
35
35
|
function SwarmSummaryCards({ counts }) {
|
|
@@ -49,119 +49,75 @@ function SwarmSummaryCards({ counts }) {
|
|
|
49
49
|
card.label
|
|
50
50
|
)) });
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const conclusionBadgeStyles = {
|
|
53
|
+
success: "bg-green-500/10 text-green-500",
|
|
54
|
+
failure: "bg-red-500/10 text-red-500",
|
|
55
|
+
cancelled: "bg-yellow-500/10 text-yellow-500",
|
|
56
|
+
skipped: "bg-muted text-muted-foreground"
|
|
57
|
+
};
|
|
58
|
+
function SwarmWorkflowList({ runs, onCancel, onRerun }) {
|
|
59
|
+
if (!runs || runs.length === 0) {
|
|
60
|
+
return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground py-4 text-center", children: "No workflow runs." });
|
|
55
61
|
}
|
|
56
|
-
return /* @__PURE__ */ jsx("div", { className: "flex flex-col
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
onClick: () => onCancel(job.run_id),
|
|
87
|
-
className: "inline-flex items-center justify-center rounded-md p-1.5 text-muted-foreground hover:bg-destructive/10 hover:text-destructive",
|
|
88
|
-
title: "Cancel job",
|
|
89
|
-
children: /* @__PURE__ */ jsx(StopIcon, { size: 14 })
|
|
90
|
-
}
|
|
91
|
-
)
|
|
92
|
-
] })
|
|
93
|
-
] }),
|
|
94
|
-
job.current_step && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-2 truncate", children: job.current_step }),
|
|
95
|
-
job.steps_total > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
96
|
-
/* @__PURE__ */ jsx("div", { className: "flex-1 h-1.5 rounded-full bg-border overflow-hidden", children: /* @__PURE__ */ jsx(
|
|
97
|
-
"div",
|
|
62
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-col divide-y divide-border", children: runs.map((run) => {
|
|
63
|
+
const isActive = run.status === "in_progress" || run.status === "queued";
|
|
64
|
+
const isRunning = run.status === "in_progress";
|
|
65
|
+
const isQueued = run.status === "queued";
|
|
66
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-3 px-1", children: [
|
|
67
|
+
isRunning && /* @__PURE__ */ jsx("span", { className: "inline-block h-2.5 w-2.5 shrink-0 rounded-full bg-green-500 animate-pulse" }),
|
|
68
|
+
isQueued && /* @__PURE__ */ jsx("span", { className: "inline-block h-2.5 w-2.5 shrink-0 rounded-full bg-yellow-500" }),
|
|
69
|
+
!isActive && /* @__PURE__ */ jsx(
|
|
70
|
+
"span",
|
|
71
|
+
{
|
|
72
|
+
className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium uppercase shrink-0 ${conclusionBadgeStyles[run.conclusion] || "bg-muted text-muted-foreground"}`,
|
|
73
|
+
children: run.conclusion || "unknown"
|
|
74
|
+
}
|
|
75
|
+
),
|
|
76
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium truncate", children: run.workflow_name || run.branch }),
|
|
77
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground shrink-0", children: isActive ? formatDuration(run.duration_seconds) : timeAgo(run.updated_at || run.started_at) }),
|
|
78
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1" }),
|
|
79
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
|
|
80
|
+
run.html_url && /* @__PURE__ */ jsx(
|
|
81
|
+
"a",
|
|
82
|
+
{
|
|
83
|
+
href: run.html_url,
|
|
84
|
+
target: "_blank",
|
|
85
|
+
rel: "noopener noreferrer",
|
|
86
|
+
className: "text-xs text-blue-500 hover:underline",
|
|
87
|
+
children: "View"
|
|
88
|
+
}
|
|
89
|
+
),
|
|
90
|
+
isActive && /* @__PURE__ */ jsx(
|
|
91
|
+
"button",
|
|
98
92
|
{
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
onClick: () => onCancel(run.run_id),
|
|
94
|
+
className: "inline-flex items-center justify-center rounded-md p-1.5 text-muted-foreground hover:bg-destructive/10 hover:text-destructive",
|
|
95
|
+
title: "Cancel",
|
|
96
|
+
children: /* @__PURE__ */ jsx(StopIcon, { size: 14 })
|
|
101
97
|
}
|
|
102
|
-
)
|
|
103
|
-
/* @__PURE__ */
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
98
|
+
),
|
|
99
|
+
!isActive && /* @__PURE__ */ jsx(
|
|
100
|
+
"button",
|
|
101
|
+
{
|
|
102
|
+
onClick: () => onRerun(run.run_id, false),
|
|
103
|
+
className: "text-xs rounded-md px-2 py-1 border hover:bg-accent",
|
|
104
|
+
children: "Rerun"
|
|
105
|
+
}
|
|
106
|
+
),
|
|
107
|
+
!isActive && run.conclusion === "failure" && /* @__PURE__ */ jsx(
|
|
108
|
+
"button",
|
|
109
|
+
{
|
|
110
|
+
onClick: () => onRerun(run.run_id, true),
|
|
111
|
+
className: "text-xs rounded-md px-2 py-1 border text-red-500 hover:bg-red-500/10",
|
|
112
|
+
children: "Rerun failed"
|
|
113
|
+
}
|
|
114
|
+
)
|
|
108
115
|
] })
|
|
109
|
-
] },
|
|
116
|
+
] }, run.run_id);
|
|
110
117
|
}) });
|
|
111
118
|
}
|
|
112
|
-
function SwarmJobHistory({ jobs, onRerun }) {
|
|
113
|
-
if (!jobs || jobs.length === 0) {
|
|
114
|
-
return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground py-4 text-center", children: "No completed jobs." });
|
|
115
|
-
}
|
|
116
|
-
const badgeStyles = {
|
|
117
|
-
success: "bg-green-500/10 text-green-500",
|
|
118
|
-
failure: "bg-red-500/10 text-red-500",
|
|
119
|
-
cancelled: "bg-yellow-500/10 text-yellow-500"
|
|
120
|
-
};
|
|
121
|
-
return /* @__PURE__ */ jsx("div", { className: "flex flex-col divide-y divide-border", children: jobs.map((job) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-3 px-1", children: [
|
|
122
|
-
/* @__PURE__ */ jsx(
|
|
123
|
-
"span",
|
|
124
|
-
{
|
|
125
|
-
className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium uppercase ${badgeStyles[job.conclusion] || "bg-muted text-muted-foreground"}`,
|
|
126
|
-
children: job.conclusion || "unknown"
|
|
127
|
-
}
|
|
128
|
-
),
|
|
129
|
-
/* @__PURE__ */ jsx("span", { className: "font-mono text-sm", children: job.job_id.slice(0, 8) }),
|
|
130
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: timeAgo(job.updated_at || job.started_at) }),
|
|
131
|
-
/* @__PURE__ */ jsx("div", { className: "flex-1" }),
|
|
132
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
133
|
-
job.html_url && /* @__PURE__ */ jsx(
|
|
134
|
-
"a",
|
|
135
|
-
{
|
|
136
|
-
href: job.html_url,
|
|
137
|
-
target: "_blank",
|
|
138
|
-
rel: "noopener noreferrer",
|
|
139
|
-
className: "text-xs text-blue-500 hover:underline",
|
|
140
|
-
children: "View"
|
|
141
|
-
}
|
|
142
|
-
),
|
|
143
|
-
/* @__PURE__ */ jsx(
|
|
144
|
-
"button",
|
|
145
|
-
{
|
|
146
|
-
onClick: () => onRerun(job.run_id, false),
|
|
147
|
-
className: "text-xs rounded-md px-2 py-1 border hover:bg-accent",
|
|
148
|
-
children: "Rerun"
|
|
149
|
-
}
|
|
150
|
-
),
|
|
151
|
-
job.conclusion === "failure" && /* @__PURE__ */ jsx(
|
|
152
|
-
"button",
|
|
153
|
-
{
|
|
154
|
-
onClick: () => onRerun(job.run_id, true),
|
|
155
|
-
className: "text-xs rounded-md px-2 py-1 border text-red-500 hover:bg-red-500/10",
|
|
156
|
-
children: "Rerun failed"
|
|
157
|
-
}
|
|
158
|
-
)
|
|
159
|
-
] })
|
|
160
|
-
] }, job.run_id)) });
|
|
161
|
-
}
|
|
162
119
|
function SwarmPage({ session }) {
|
|
163
|
-
const [
|
|
164
|
-
const [completed, setCompleted] = useState([]);
|
|
120
|
+
const [runs, setRuns] = useState([]);
|
|
165
121
|
const [counts, setCounts] = useState({ running: 0, queued: 0 });
|
|
166
122
|
const [hasMore, setHasMore] = useState(false);
|
|
167
123
|
const [page, setPage] = useState(1);
|
|
@@ -171,11 +127,10 @@ function SwarmPage({ session }) {
|
|
|
171
127
|
const fetchStatus = useCallback(async () => {
|
|
172
128
|
try {
|
|
173
129
|
const data = await getSwarmStatus();
|
|
174
|
-
setActive(data.active || []);
|
|
175
130
|
setCounts(data.counts || { running: 0, queued: 0 });
|
|
176
131
|
setHasMore(data.hasMore || false);
|
|
177
|
-
|
|
178
|
-
const firstPage = data.
|
|
132
|
+
setRuns((prev) => {
|
|
133
|
+
const firstPage = data.runs || [];
|
|
179
134
|
if (page <= 1) return firstPage;
|
|
180
135
|
const beyondPage1 = prev.slice(firstPage.length);
|
|
181
136
|
return [...firstPage, ...beyondPage1];
|
|
@@ -197,9 +152,8 @@ function SwarmPage({ session }) {
|
|
|
197
152
|
setPage(1);
|
|
198
153
|
try {
|
|
199
154
|
const data = await getSwarmStatus();
|
|
200
|
-
setActive(data.active || []);
|
|
201
155
|
setCounts(data.counts || { running: 0, queued: 0 });
|
|
202
|
-
|
|
156
|
+
setRuns(data.runs || []);
|
|
203
157
|
setHasMore(data.hasMore || false);
|
|
204
158
|
} catch (err) {
|
|
205
159
|
console.error("Failed to fetch swarm status:", err);
|
|
@@ -211,7 +165,7 @@ function SwarmPage({ session }) {
|
|
|
211
165
|
setLoadingMore(true);
|
|
212
166
|
try {
|
|
213
167
|
const data = await getSwarmStatus(nextPage);
|
|
214
|
-
|
|
168
|
+
setRuns((prev) => [...prev, ...data.runs || []]);
|
|
215
169
|
setHasMore(data.hasMore || false);
|
|
216
170
|
setPage(nextPage);
|
|
217
171
|
} catch (err) {
|
|
@@ -257,21 +211,12 @@ function SwarmPage({ session }) {
|
|
|
257
211
|
loading ? /* @__PURE__ */ jsx(LoadingSkeleton, {}) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-6", children: [
|
|
258
212
|
/* @__PURE__ */ jsx(SwarmSummaryCards, { counts }),
|
|
259
213
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
260
|
-
/* @__PURE__ */ jsx("h2", { className: "text-sm font-medium text-muted-foreground uppercase tracking-wide mb-3", children: "
|
|
261
|
-
/* @__PURE__ */ jsx(
|
|
262
|
-
SwarmActiveJobs,
|
|
263
|
-
{
|
|
264
|
-
jobs: active,
|
|
265
|
-
onCancel: handleCancel
|
|
266
|
-
}
|
|
267
|
-
)
|
|
268
|
-
] }),
|
|
269
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
270
|
-
/* @__PURE__ */ jsx("h2", { className: "text-sm font-medium text-muted-foreground uppercase tracking-wide mb-3", children: "Job History" }),
|
|
214
|
+
/* @__PURE__ */ jsx("h2", { className: "text-sm font-medium text-muted-foreground uppercase tracking-wide mb-3", children: "Workflow Runs" }),
|
|
271
215
|
/* @__PURE__ */ jsx(
|
|
272
|
-
|
|
216
|
+
SwarmWorkflowList,
|
|
273
217
|
{
|
|
274
|
-
|
|
218
|
+
runs,
|
|
219
|
+
onCancel: handleCancel,
|
|
275
220
|
onRerun: handleRerun
|
|
276
221
|
}
|
|
277
222
|
),
|
|
@@ -42,7 +42,7 @@ function LoadingSkeleton() {
|
|
|
42
42
|
</div>
|
|
43
43
|
<div className="h-8 w-32 animate-pulse rounded-md bg-border/50" />
|
|
44
44
|
{[...Array(3)].map((_, i) => (
|
|
45
|
-
<div key={i} className="h-
|
|
45
|
+
<div key={i} className="h-14 animate-pulse rounded-md bg-border/50" />
|
|
46
46
|
))}
|
|
47
47
|
</div>
|
|
48
48
|
);
|
|
@@ -74,90 +74,104 @@ function SwarmSummaryCards({ counts }) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
77
|
-
//
|
|
77
|
+
// Unified Workflow List
|
|
78
78
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
const conclusionBadgeStyles = {
|
|
81
|
+
success: 'bg-green-500/10 text-green-500',
|
|
82
|
+
failure: 'bg-red-500/10 text-red-500',
|
|
83
|
+
cancelled: 'bg-yellow-500/10 text-yellow-500',
|
|
84
|
+
skipped: 'bg-muted text-muted-foreground',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function SwarmWorkflowList({ runs, onCancel, onRerun }) {
|
|
88
|
+
if (!runs || runs.length === 0) {
|
|
82
89
|
return (
|
|
83
90
|
<div className="text-sm text-muted-foreground py-4 text-center">
|
|
84
|
-
No
|
|
91
|
+
No workflow runs.
|
|
85
92
|
</div>
|
|
86
93
|
);
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
return (
|
|
90
|
-
<div className="flex flex-col
|
|
91
|
-
{
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
: 0;
|
|
97
|
+
<div className="flex flex-col divide-y divide-border">
|
|
98
|
+
{runs.map((run) => {
|
|
99
|
+
const isActive = run.status === 'in_progress' || run.status === 'queued';
|
|
100
|
+
const isRunning = run.status === 'in_progress';
|
|
101
|
+
const isQueued = run.status === 'queued';
|
|
96
102
|
|
|
97
103
|
return (
|
|
98
|
-
<div key={
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
104
|
+
<div key={run.run_id} className="flex items-center gap-3 py-3 px-1">
|
|
105
|
+
{/* Status indicator */}
|
|
106
|
+
{isRunning && (
|
|
107
|
+
<span className="inline-block h-2.5 w-2.5 shrink-0 rounded-full bg-green-500 animate-pulse" />
|
|
108
|
+
)}
|
|
109
|
+
{isQueued && (
|
|
110
|
+
<span className="inline-block h-2.5 w-2.5 shrink-0 rounded-full bg-yellow-500" />
|
|
111
|
+
)}
|
|
112
|
+
{!isActive && (
|
|
113
|
+
<span
|
|
114
|
+
className={`inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium uppercase shrink-0 ${
|
|
115
|
+
conclusionBadgeStyles[run.conclusion] || 'bg-muted text-muted-foreground'
|
|
116
|
+
}`}
|
|
117
|
+
>
|
|
118
|
+
{run.conclusion || 'unknown'}
|
|
119
|
+
</span>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{/* Workflow name */}
|
|
123
|
+
<span className="text-sm font-medium truncate">
|
|
124
|
+
{run.workflow_name || run.branch}
|
|
125
|
+
</span>
|
|
126
|
+
|
|
127
|
+
{/* Duration or time ago */}
|
|
128
|
+
<span className="text-xs text-muted-foreground shrink-0">
|
|
129
|
+
{isActive
|
|
130
|
+
? formatDuration(run.duration_seconds)
|
|
131
|
+
: timeAgo(run.updated_at || run.started_at)}
|
|
132
|
+
</span>
|
|
133
|
+
|
|
134
|
+
{/* Spacer */}
|
|
135
|
+
<div className="flex-1" />
|
|
136
|
+
|
|
137
|
+
{/* Actions */}
|
|
138
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
139
|
+
{run.html_url && (
|
|
140
|
+
<a
|
|
141
|
+
href={run.html_url}
|
|
142
|
+
target="_blank"
|
|
143
|
+
rel="noopener noreferrer"
|
|
144
|
+
className="text-xs text-blue-500 hover:underline"
|
|
145
|
+
>
|
|
146
|
+
View
|
|
147
|
+
</a>
|
|
148
|
+
)}
|
|
149
|
+
{isActive && (
|
|
130
150
|
<button
|
|
131
|
-
onClick={() => onCancel(
|
|
151
|
+
onClick={() => onCancel(run.run_id)}
|
|
132
152
|
className="inline-flex items-center justify-center rounded-md p-1.5 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
|
|
133
|
-
title="Cancel
|
|
153
|
+
title="Cancel"
|
|
134
154
|
>
|
|
135
155
|
<StopIcon size={14} />
|
|
136
156
|
</button>
|
|
137
|
-
|
|
157
|
+
)}
|
|
158
|
+
{!isActive && (
|
|
159
|
+
<button
|
|
160
|
+
onClick={() => onRerun(run.run_id, false)}
|
|
161
|
+
className="text-xs rounded-md px-2 py-1 border hover:bg-accent"
|
|
162
|
+
>
|
|
163
|
+
Rerun
|
|
164
|
+
</button>
|
|
165
|
+
)}
|
|
166
|
+
{!isActive && run.conclusion === 'failure' && (
|
|
167
|
+
<button
|
|
168
|
+
onClick={() => onRerun(run.run_id, true)}
|
|
169
|
+
className="text-xs rounded-md px-2 py-1 border text-red-500 hover:bg-red-500/10"
|
|
170
|
+
>
|
|
171
|
+
Rerun failed
|
|
172
|
+
</button>
|
|
173
|
+
)}
|
|
138
174
|
</div>
|
|
139
|
-
|
|
140
|
-
{/* Current step */}
|
|
141
|
-
{job.current_step && (
|
|
142
|
-
<p className="text-xs text-muted-foreground mb-2 truncate">
|
|
143
|
-
{job.current_step}
|
|
144
|
-
</p>
|
|
145
|
-
)}
|
|
146
|
-
|
|
147
|
-
{/* Progress bar */}
|
|
148
|
-
{job.steps_total > 0 && (
|
|
149
|
-
<div className="flex items-center gap-2">
|
|
150
|
-
<div className="flex-1 h-1.5 rounded-full bg-border overflow-hidden">
|
|
151
|
-
<div
|
|
152
|
-
className="h-full rounded-full bg-green-500 transition-all duration-500"
|
|
153
|
-
style={{ width: `${progress}%` }}
|
|
154
|
-
/>
|
|
155
|
-
</div>
|
|
156
|
-
<span className="text-xs text-muted-foreground shrink-0">
|
|
157
|
-
{job.steps_completed}/{job.steps_total}
|
|
158
|
-
</span>
|
|
159
|
-
</div>
|
|
160
|
-
)}
|
|
161
175
|
</div>
|
|
162
176
|
);
|
|
163
177
|
})}
|
|
@@ -165,89 +179,12 @@ function SwarmActiveJobs({ jobs, onCancel }) {
|
|
|
165
179
|
);
|
|
166
180
|
}
|
|
167
181
|
|
|
168
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
169
|
-
// Job History
|
|
170
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
171
|
-
|
|
172
|
-
function SwarmJobHistory({ jobs, onRerun }) {
|
|
173
|
-
if (!jobs || jobs.length === 0) {
|
|
174
|
-
return (
|
|
175
|
-
<div className="text-sm text-muted-foreground py-4 text-center">
|
|
176
|
-
No completed jobs.
|
|
177
|
-
</div>
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const badgeStyles = {
|
|
182
|
-
success: 'bg-green-500/10 text-green-500',
|
|
183
|
-
failure: 'bg-red-500/10 text-red-500',
|
|
184
|
-
cancelled: 'bg-yellow-500/10 text-yellow-500',
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
return (
|
|
188
|
-
<div className="flex flex-col divide-y divide-border">
|
|
189
|
-
{jobs.map((job) => (
|
|
190
|
-
<div key={job.run_id} className="flex items-center gap-3 py-3 px-1">
|
|
191
|
-
{/* Conclusion badge */}
|
|
192
|
-
<span
|
|
193
|
-
className={`inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium uppercase ${
|
|
194
|
-
badgeStyles[job.conclusion] || 'bg-muted text-muted-foreground'
|
|
195
|
-
}`}
|
|
196
|
-
>
|
|
197
|
-
{job.conclusion || 'unknown'}
|
|
198
|
-
</span>
|
|
199
|
-
|
|
200
|
-
{/* Job ID */}
|
|
201
|
-
<span className="font-mono text-sm">{job.job_id.slice(0, 8)}</span>
|
|
202
|
-
|
|
203
|
-
{/* Time ago */}
|
|
204
|
-
<span className="text-xs text-muted-foreground">
|
|
205
|
-
{timeAgo(job.updated_at || job.started_at)}
|
|
206
|
-
</span>
|
|
207
|
-
|
|
208
|
-
{/* Spacer */}
|
|
209
|
-
<div className="flex-1" />
|
|
210
|
-
|
|
211
|
-
{/* Actions */}
|
|
212
|
-
<div className="flex items-center gap-2">
|
|
213
|
-
{job.html_url && (
|
|
214
|
-
<a
|
|
215
|
-
href={job.html_url}
|
|
216
|
-
target="_blank"
|
|
217
|
-
rel="noopener noreferrer"
|
|
218
|
-
className="text-xs text-blue-500 hover:underline"
|
|
219
|
-
>
|
|
220
|
-
View
|
|
221
|
-
</a>
|
|
222
|
-
)}
|
|
223
|
-
<button
|
|
224
|
-
onClick={() => onRerun(job.run_id, false)}
|
|
225
|
-
className="text-xs rounded-md px-2 py-1 border hover:bg-accent"
|
|
226
|
-
>
|
|
227
|
-
Rerun
|
|
228
|
-
</button>
|
|
229
|
-
{job.conclusion === 'failure' && (
|
|
230
|
-
<button
|
|
231
|
-
onClick={() => onRerun(job.run_id, true)}
|
|
232
|
-
className="text-xs rounded-md px-2 py-1 border text-red-500 hover:bg-red-500/10"
|
|
233
|
-
>
|
|
234
|
-
Rerun failed
|
|
235
|
-
</button>
|
|
236
|
-
)}
|
|
237
|
-
</div>
|
|
238
|
-
</div>
|
|
239
|
-
))}
|
|
240
|
-
</div>
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
182
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
245
183
|
// Main Page
|
|
246
184
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
247
185
|
|
|
248
186
|
export function SwarmPage({ session }) {
|
|
249
|
-
const [
|
|
250
|
-
const [completed, setCompleted] = useState([]);
|
|
187
|
+
const [runs, setRuns] = useState([]);
|
|
251
188
|
const [counts, setCounts] = useState({ running: 0, queued: 0 });
|
|
252
189
|
const [hasMore, setHasMore] = useState(false);
|
|
253
190
|
const [page, setPage] = useState(1);
|
|
@@ -258,15 +195,12 @@ export function SwarmPage({ session }) {
|
|
|
258
195
|
const fetchStatus = useCallback(async () => {
|
|
259
196
|
try {
|
|
260
197
|
const data = await getSwarmStatus();
|
|
261
|
-
setActive(data.active || []);
|
|
262
198
|
setCounts(data.counts || { running: 0, queued: 0 });
|
|
263
199
|
setHasMore(data.hasMore || false);
|
|
264
|
-
// On auto-refresh, replace page 1
|
|
265
|
-
|
|
266
|
-
const firstPage = data.
|
|
267
|
-
// If user hasn't loaded beyond page 1, just use page 1 results
|
|
200
|
+
// On auto-refresh, replace page 1 runs but keep any loaded beyond page 1
|
|
201
|
+
setRuns((prev) => {
|
|
202
|
+
const firstPage = data.runs || [];
|
|
268
203
|
if (page <= 1) return firstPage;
|
|
269
|
-
// Keep accumulated jobs beyond page 1
|
|
270
204
|
const beyondPage1 = prev.slice(firstPage.length);
|
|
271
205
|
return [...firstPage, ...beyondPage1];
|
|
272
206
|
});
|
|
@@ -286,13 +220,11 @@ export function SwarmPage({ session }) {
|
|
|
286
220
|
|
|
287
221
|
const handleRefresh = async () => {
|
|
288
222
|
setRefreshing(true);
|
|
289
|
-
// Reset to page 1
|
|
290
223
|
setPage(1);
|
|
291
224
|
try {
|
|
292
225
|
const data = await getSwarmStatus();
|
|
293
|
-
setActive(data.active || []);
|
|
294
226
|
setCounts(data.counts || { running: 0, queued: 0 });
|
|
295
|
-
|
|
227
|
+
setRuns(data.runs || []);
|
|
296
228
|
setHasMore(data.hasMore || false);
|
|
297
229
|
} catch (err) {
|
|
298
230
|
console.error('Failed to fetch swarm status:', err);
|
|
@@ -305,7 +237,7 @@ export function SwarmPage({ session }) {
|
|
|
305
237
|
setLoadingMore(true);
|
|
306
238
|
try {
|
|
307
239
|
const data = await getSwarmStatus(nextPage);
|
|
308
|
-
|
|
240
|
+
setRuns((prev) => [...prev, ...(data.runs || [])]);
|
|
309
241
|
setHasMore(data.hasMore || false);
|
|
310
242
|
setPage(nextPage);
|
|
311
243
|
} catch (err) {
|
|
@@ -365,24 +297,14 @@ export function SwarmPage({ session }) {
|
|
|
365
297
|
{/* Summary Cards */}
|
|
366
298
|
<SwarmSummaryCards counts={counts} />
|
|
367
299
|
|
|
368
|
-
{/*
|
|
300
|
+
{/* Workflow Runs */}
|
|
369
301
|
<div>
|
|
370
302
|
<h2 className="text-sm font-medium text-muted-foreground uppercase tracking-wide mb-3">
|
|
371
|
-
|
|
303
|
+
Workflow Runs
|
|
372
304
|
</h2>
|
|
373
|
-
<
|
|
374
|
-
|
|
305
|
+
<SwarmWorkflowList
|
|
306
|
+
runs={runs}
|
|
375
307
|
onCancel={handleCancel}
|
|
376
|
-
/>
|
|
377
|
-
</div>
|
|
378
|
-
|
|
379
|
-
{/* Job History */}
|
|
380
|
-
<div>
|
|
381
|
-
<h2 className="text-sm font-medium text-muted-foreground uppercase tracking-wide mb-3">
|
|
382
|
-
Job History
|
|
383
|
-
</h2>
|
|
384
|
-
<SwarmJobHistory
|
|
385
|
-
jobs={completed}
|
|
386
308
|
onRerun={handleRerun}
|
|
387
309
|
/>
|
|
388
310
|
{hasMore && (
|
package/lib/tools/github.js
CHANGED
|
@@ -30,7 +30,7 @@ async function githubApi(endpoint, options = {}) {
|
|
|
30
30
|
* @param {string} [workflow] - Workflow filename to scope to (e.g., 'run-job.yml')
|
|
31
31
|
* @returns {Promise<object>} - Workflow runs response
|
|
32
32
|
*/
|
|
33
|
-
async function getWorkflowRuns(status, workflow,
|
|
33
|
+
async function getWorkflowRuns(status, { workflow, page = 1, perPage = 100 } = {}) {
|
|
34
34
|
const { GH_OWNER, GH_REPO } = process.env;
|
|
35
35
|
const params = new URLSearchParams();
|
|
36
36
|
if (status) params.set('status', status);
|
|
@@ -62,8 +62,8 @@ async function getWorkflowRunJobs(runId) {
|
|
|
62
62
|
async function getJobStatus(jobId) {
|
|
63
63
|
// Fetch both in_progress and queued runs (scoped to run-job.yml)
|
|
64
64
|
const [inProgress, queued] = await Promise.all([
|
|
65
|
-
getWorkflowRuns('in_progress', 'run-job.yml'),
|
|
66
|
-
getWorkflowRuns('queued', 'run-job.yml'),
|
|
65
|
+
getWorkflowRuns('in_progress', { workflow: 'run-job.yml' }),
|
|
66
|
+
getWorkflowRuns('queued', { workflow: 'run-job.yml' }),
|
|
67
67
|
]);
|
|
68
68
|
|
|
69
69
|
const allRuns = [...(inProgress.workflow_runs || []), ...(queued.workflow_runs || [])];
|
|
@@ -125,85 +125,35 @@ async function getJobStatus(jobId) {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
|
-
* Get full swarm status:
|
|
129
|
-
* @
|
|
128
|
+
* Get full swarm status: unified list of all workflow runs with counts
|
|
129
|
+
* @param {number} [page=1] - Page number for pagination
|
|
130
|
+
* @returns {Promise<object>} - { runs, hasMore, counts }
|
|
130
131
|
*/
|
|
131
132
|
async function getSwarmStatus(page = 1) {
|
|
132
|
-
const [
|
|
133
|
-
getWorkflowRuns('in_progress',
|
|
134
|
-
getWorkflowRuns('queued',
|
|
135
|
-
getWorkflowRuns(
|
|
133
|
+
const [inProgressData, queuedData, allRuns] = await Promise.all([
|
|
134
|
+
getWorkflowRuns('in_progress', { perPage: 1 }),
|
|
135
|
+
getWorkflowRuns('queued', { perPage: 1 }),
|
|
136
|
+
getWorkflowRuns(null, { page, perPage: 25 }),
|
|
136
137
|
]);
|
|
137
138
|
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
...(queued.workflow_runs || []),
|
|
141
|
-
].filter(run => run.head_branch?.startsWith('job/'));
|
|
142
|
-
|
|
143
|
-
const completedRuns = (completed.workflow_runs || [])
|
|
144
|
-
.filter(run => run.head_branch?.startsWith('job/'));
|
|
145
|
-
|
|
146
|
-
// Get step details for active jobs
|
|
147
|
-
const active = await Promise.all(
|
|
148
|
-
activeRuns.map(async (run) => {
|
|
149
|
-
const jobId = run.head_branch.slice(4);
|
|
150
|
-
const startedAt = new Date(run.created_at);
|
|
151
|
-
const durationSeconds = Math.round((Date.now() - startedAt.getTime()) / 1000);
|
|
152
|
-
|
|
153
|
-
let currentStep = null;
|
|
154
|
-
let stepsCompleted = 0;
|
|
155
|
-
let stepsTotal = 0;
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
const jobsData = await getWorkflowRunJobs(run.id);
|
|
159
|
-
if (jobsData.jobs?.length > 0) {
|
|
160
|
-
const job = jobsData.jobs[0];
|
|
161
|
-
stepsTotal = job.steps?.length || 0;
|
|
162
|
-
stepsCompleted = job.steps?.filter(s => s.status === 'completed').length || 0;
|
|
163
|
-
currentStep = job.steps?.find(s => s.status === 'in_progress')?.name || null;
|
|
164
|
-
}
|
|
165
|
-
} catch (err) {
|
|
166
|
-
// Jobs endpoint may fail if run hasn't started yet
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
job_id: jobId,
|
|
171
|
-
branch: run.head_branch,
|
|
172
|
-
status: run.status,
|
|
173
|
-
workflow_name: run.name,
|
|
174
|
-
started_at: run.created_at,
|
|
175
|
-
duration_seconds: durationSeconds,
|
|
176
|
-
current_step: currentStep,
|
|
177
|
-
steps_completed: stepsCompleted,
|
|
178
|
-
steps_total: stepsTotal,
|
|
179
|
-
run_id: run.id,
|
|
180
|
-
html_url: run.html_url,
|
|
181
|
-
};
|
|
182
|
-
})
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
// Completed jobs are lighter — no step detail needed
|
|
186
|
-
const completedJobs = completedRuns.map((run) => ({
|
|
187
|
-
job_id: run.head_branch.slice(4),
|
|
139
|
+
const runs = (allRuns.workflow_runs || []).map(run => ({
|
|
140
|
+
run_id: run.id,
|
|
188
141
|
branch: run.head_branch,
|
|
189
142
|
status: run.status,
|
|
190
143
|
conclusion: run.conclusion,
|
|
191
144
|
workflow_name: run.name,
|
|
192
145
|
started_at: run.created_at,
|
|
193
146
|
updated_at: run.updated_at,
|
|
194
|
-
|
|
147
|
+
duration_seconds: Math.round((Date.now() - new Date(run.created_at).getTime()) / 1000),
|
|
195
148
|
html_url: run.html_url,
|
|
196
149
|
}));
|
|
197
150
|
|
|
198
|
-
const totalCompleted = completed.total_count || 0;
|
|
199
|
-
|
|
200
151
|
return {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
hasMore: page * 100 < totalCompleted,
|
|
152
|
+
runs,
|
|
153
|
+
hasMore: page * 25 < (allRuns.total_count || 0),
|
|
204
154
|
counts: {
|
|
205
|
-
running:
|
|
206
|
-
queued:
|
|
155
|
+
running: inProgressData.total_count || 0,
|
|
156
|
+
queued: queuedData.total_count || 0,
|
|
207
157
|
},
|
|
208
158
|
};
|
|
209
159
|
}
|
package/package.json
CHANGED
|
@@ -13,6 +13,7 @@ jobs:
|
|
|
13
13
|
steps:
|
|
14
14
|
- name: Upgrade thepopebot
|
|
15
15
|
run: |
|
|
16
|
+
# Phase 1: Update package and build (but don't swap yet)
|
|
16
17
|
docker exec thepopebot-event-handler bash -c '
|
|
17
18
|
export GH_TOKEN=$(grep "^GH_TOKEN=" /app/.env | cut -d= -f2-)
|
|
18
19
|
echo "${GH_TOKEN}" | gh auth login --with-token
|
|
@@ -21,19 +22,23 @@ jobs:
|
|
|
21
22
|
git fetch origin main
|
|
22
23
|
git reset --hard origin/main
|
|
23
24
|
|
|
24
|
-
# Update thepopebot package
|
|
25
25
|
npm update thepopebot
|
|
26
|
+
npx thepopebot init
|
|
26
27
|
npm install --omit=dev
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
rm -rf .next-new .next-old
|
|
29
|
+
rm -rf .next-new
|
|
30
30
|
NEXT_BUILD_DIR=.next-new npm run build
|
|
31
|
+
'
|
|
32
|
+
|
|
33
|
+
# Phase 2: Pull new image and restart container
|
|
34
|
+
cd /project
|
|
35
|
+
docker compose pull event-handler
|
|
36
|
+
docker compose up -d event-handler
|
|
31
37
|
|
|
38
|
+
# Phase 3: Swap build and reload inside new container
|
|
39
|
+
docker exec thepopebot-event-handler bash -c '
|
|
32
40
|
mv .next .next-old 2>/dev/null || true
|
|
33
41
|
mv .next-new .next
|
|
34
|
-
|
|
35
|
-
echo "Upgrade complete, reloading..."
|
|
36
42
|
npx pm2 reload all
|
|
37
|
-
|
|
38
43
|
rm -rf .next-old
|
|
39
44
|
'
|