@cloud-cli/on 1.11.0 โ 1.12.0
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/README.md +2 -0
- package/dist/on.js +140 -92
- package/dist/queue.d.ts +1 -0
- package/dist/queue.d.ts.map +1 -1
- package/dist/secret-repository.d.ts +1 -0
- package/dist/secret-repository.d.ts.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/workflows-ui.d.ts +2 -0
- package/dist/workflows-ui.d.ts.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -223,6 +223,8 @@ The GitHub status plugin publishes commit states when a workflow starts and fini
|
|
|
223
223
|
|
|
224
224
|
Run details are available as HTML at `/runs/:id` and as sanitized JSON at `/api/runs/:id`. The HTML view refreshes reactively through job-specific SSE events while a run is active. Both representations omit raw webhook bodies, sensitive input fields, execution environment values, and internal rerun state.
|
|
225
225
|
|
|
226
|
+
Open `/workflows` to list workflows and manage secret values. Open `/workflows/new` to create a definition or `/workflows/:id` to edit one. These pages are protected by the administrator HTTP Basic credential; secret values are write-only.
|
|
227
|
+
|
|
226
228
|
---
|
|
227
229
|
|
|
228
230
|
## ๐ Deterministic Evaluation Rules
|
package/dist/on.js
CHANGED
|
@@ -140,12 +140,16 @@ var se = class {
|
|
|
140
140
|
await S.run("UPDATE jobs SET status = ?, finished_at = CURRENT_TIMESTAMP WHERE id = ?;", [t, e]);
|
|
141
141
|
}
|
|
142
142
|
async completeJob(e, t, n) {
|
|
143
|
-
await S.run("UPDATE jobs\n SET status =
|
|
143
|
+
await S.run("UPDATE jobs\n SET status = CASE WHEN status = 'cancelled' THEN 'cancelled' ELSE ? END,\n report = ?, updated_at = CURRENT_TIMESTAMP, finished_at = CURRENT_TIMESTAMP\n WHERE id = ?;", [
|
|
144
144
|
t,
|
|
145
145
|
JSON.stringify(n),
|
|
146
146
|
e
|
|
147
147
|
]);
|
|
148
148
|
}
|
|
149
|
+
async cancelJob(e) {
|
|
150
|
+
let t = await this.getJob(e);
|
|
151
|
+
return t ? t.status !== "pending" && t.status !== "running" ? "not_active" : (await S.run("UPDATE jobs SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP, finished_at = CURRENT_TIMESTAMP\n WHERE id = ? AND status IN ('pending', 'running');", [e]), (await this.getJob(e))?.status === "cancelled" ? "cancelled" : "not_active") : "not_found";
|
|
152
|
+
}
|
|
149
153
|
async restartJob(e) {
|
|
150
154
|
let t = await this.getJob(e);
|
|
151
155
|
if (t) return t.status === "running" && await S.run("UPDATE jobs SET status = 'cancelled', finished_at = CURRENT_TIMESTAMP WHERE id = ?;", [e]), (await S.get("INSERT INTO jobs (parentId, workflow_id, workflow_revision, required_tags, concurrency_key, payload) SELECT id, workflow_id, workflow_revision, required_tags, concurrency_key, payload FROM jobs WHERE id = ? RETURNING *", [e])).id;
|
|
@@ -3651,7 +3655,7 @@ var En = class {
|
|
|
3651
3655
|
inputs: e
|
|
3652
3656
|
};
|
|
3653
3657
|
}
|
|
3654
|
-
}, Dn = "<!DOCTYPE html>\n<html lang=\"en\" class=\"dark\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Workflow Engine Dashboard</title>\n <script type=\"importmap\">\n { \"imports\": { \"@li3/\": \"https://cdn.li3.dev/@li3/\" } }\n <\/script>\n</head>\n<body class=\"bg-gray-950 text-gray-100 min-h-screen p-4 md:p-6 font-sans\">\n <template app>\n <main class=\"max-w-6xl mx-auto space-y-6\">\n <header class=\"flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-gray-800 pb-4\">\n <div>\n <h1 class=\"text-2xl font-bold text-white\">Runner Engine Status</h1>\n <p class=\"text-xs text-gray-400\">Live Job Queue & Execution Traces</p>\n </div>\n <div class=\"flex items-center gap-3 text-xs font-mono\">\n <span class=\"text-gray-500\">Updated {{ lastUpdated }}</span>\n <span\n class=\"px-3 py-1 rounded-full border\"\n bind-class=\"{\n 'bg-emerald-500/10.text-emerald-400.border-emerald-500/20': !refreshError,\n 'bg-rose-500/10.text-rose-400.border-rose-500/20': refreshError\n }\"\n >\n {{ refreshError ? 'Refresh failed' : 'System operational' }}\n </span>\n </div>\n </header>\n\n <section class=\"bg-gray-900 border border-gray-800 rounded-xl overflow-hidden hidden md:block\">\n <table class=\"w-full text-left text-sm\">\n <thead class=\"bg-gray-800/50 text-gray-400 text-xs uppercase font-mono border-b border-gray-800\">\n <tr>\n <th class=\"py-3 px-4\">Job ID</th>\n <th class=\"py-3 px-4\">Workflow</th>\n <th class=\"py-3 px-4\">Status</th>\n <th class=\"py-3 px-4 hidden xl:table-cell\">Worker</th>\n <th class=\"py-3 px-4 hidden lg:table-cell\">Created At</th>\n <th class=\"py-3 px-4 text-right\">Action</th>\n </tr>\n </thead>\n <tbody>\n <template for=\"job of jobs\">\n <tr class=\"border-b border-gray-800 hover:bg-gray-800/40 transition\">\n <td class=\"py-3 px-4 font-mono text-indigo-400\">\n <a attr-href=\"'/runs/' + job.id\" class=\"hover:underline\">#{{ job.id }}</a>\n </td>\n <td class=\"py-3 px-4 font-medium text-white\">{{ job.workflowId }}</td>\n <td class=\"py-3 px-4\">\n <span\n class=\"px-2.5 py-0.5 rounded-full text-xs font-semibold\"\n bind-class=\"{\n 'bg-emerald-500/10.text-emerald-400': job.status === 'success',\n 'bg-rose-500/10.text-rose-400': job.status === 'failed',\n 'bg-indigo-500/10.text-indigo-400.animate-pulse': job.status === 'running',\n 'bg-gray-500/10.text-gray-400': !['success', 'failed', 'running'].includes(job.status)\n }\"\n >\n {{ upper(job.status) }}\n </span>\n </td>\n <td class=\"py-3 px-4 hidden xl:table-cell text-xs font-mono text-gray-400\">{{ job.workerId || '-' }}</td>\n <td class=\"py-3 px-4 hidden lg:table-cell text-xs text-gray-400\">{{ job.createdAt }}</td>\n <td class=\"py-3 px-4 text-right whitespace-nowrap\">\n <a attr-href=\"'/runs/' + job.id\" class=\"text-xs bg-gray-800 hover:bg-gray-700 text-gray-200 px-3 py-1 rounded border border-gray-700\">View Trace</a>\n </td>\n </tr>\n </template>\n </tbody>\n </table>\n <template if=\"!jobs.length\">\n <div class=\"p-8 text-center text-gray-500\">No jobs recorded yet.</div>\n </template>\n </section>\n\n <section class=\"md:hidden bg-gray-900 border border-gray-800 rounded-xl divide-y divide-gray-800 overflow-hidden\">\n <template for=\"job of jobs\">\n <article class=\"p-4 bg-gray-900/60\">\n <div class=\"flex items-start justify-between gap-3\">\n <div class=\"min-w-0\">\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0\"\n bind-class=\"{\n 'bg-emerald-400': job.status === 'success',\n 'bg-rose-400': job.status === 'failed',\n 'bg-indigo-400.animate-pulse': job.status === 'running',\n 'bg-gray-500': !['success', 'failed', 'running'].includes(job.status)\n }\"\n ></span>\n <h2 class=\"font-medium text-white truncate\">{{ job.workflowId }}</h2>\n </div>\n <p class=\"mt-1 pl-[18px] text-xs font-mono text-gray-400\">#{{ job.id }} ยท {{ upper(job.status) }}</p>\n </div>\n <a attr-href=\"'/runs/' + job.id\" class=\"shrink-0 text-xs bg-gray-800 hover:bg-gray-700 text-gray-300 px-2.5 py-1 rounded border border-gray-700\">Trace</a>\n </div>\n </article>\n </template>\n <template if=\"!jobs.length\">\n <div class=\"p-8 text-center text-gray-500\">No jobs recorded yet.</div>\n </template>\n </section>\n\n <template if=\"hasMore\">\n <div class=\"flex justify-center\">\n <button\n class=\"px-4 py-2 text-sm text-gray-200 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg\"\n on-click=\"loadMore()\"\n >\n {{ loadingMore ? 'Loading...' : 'Load more' }}\n </button>\n </div>\n </template>\n </main>\n\n <script setup>\n import { onDestroy, onInit, ref } from '@li3/web';\n\n export default function setup() {\n const jobs = ref([]);\n const lastUpdated = ref('now');\n const refreshError = ref(false);\n const hasMore = ref(false);\n const loadingMore = ref(false);\n let refreshTimer;\n let eventSource;\n let refreshing = false;\n let refreshPending = false;\n\n const upper = (value) => String(value || '').toUpperCase();\n const terminalStatuses = new Set(['success', 'failed', 'cancelled']);\n const refreshJobs = async () => {\n if (refreshing) {\n refreshPending = true;\n return;\n }\n refreshing = true;\n\n try {\n const activeIds = jobs.value\n .filter((job) => !terminalStatuses.has(job.status))\n .map((job) => Number(job.id));\n const knownIds = jobs.value.map((job) => Number(job.id));\n const afterId = activeIds.length\n ? Math.max(0, Math.min(...activeIds) - 1)\n : knownIds.length\n ? Math.max(...knownIds)\n : null;\n const url = afterId === null ? '/api/jobs?limit=500' : `/api/jobs?afterId=${afterId}&limit=500`;\n const response = await fetch(url, { headers: { accept: 'application/json' } });\n if (!response.ok) throw new Error(`Dashboard refresh failed: ${response.status}`);\n\n const data = await response.json();\n const merged = new Map(jobs.value.map((job) => [job.id, job]));\n for (const job of data.jobs) merged.set(job.id, job);\n jobs.value = Array.from(merged.values()).sort((a, b) => Number(b.id) - Number(a.id));\n lastUpdated.value = new Date().toLocaleTimeString();\n refreshError.value = false;\n } catch (error) {\n console.error(error);\n refreshError.value = true;\n } finally {\n refreshing = false;\n if (refreshPending) {\n refreshPending = false;\n void refreshJobs();\n }\n }\n };\n\n const loadMore = async () => {\n if (loadingMore.value || !hasMore.value || !jobs.value.length) return;\n loadingMore.value = true;\n\n try {\n const beforeId = Math.min(...jobs.value.map((job) => Number(job.id)));\n const response = await fetch(`/api/jobs?beforeId=${beforeId}`, {\n headers: { accept: 'application/json' },\n });\n if (!response.ok) throw new Error(`Loading older jobs failed: ${response.status}`);\n\n const data = await response.json();\n const merged = new Map(jobs.value.map((job) => [job.id, job]));\n for (const job of data.jobs) merged.set(job.id, job);\n jobs.value = Array.from(merged.values()).sort((a, b) => Number(b.id) - Number(a.id));\n hasMore.value = data.hasMore;\n } catch (error) {\n console.error(error);\n refreshError.value = true;\n } finally {\n loadingMore.value = false;\n }\n };\n\n onInit(() => {\n eventSource = new EventSource('/api/events');\n eventSource.addEventListener('jobs.available', refreshJobs);\n eventSource.addEventListener('jobs.changed', refreshJobs);\n eventSource.onerror = () => {\n refreshError.value = true;\n };\n refreshTimer = setInterval(refreshJobs, 60000);\n });\n onDestroy(() => {\n eventSource?.close();\n clearInterval(refreshTimer);\n });\n\n return { jobs, lastUpdated, refreshError, hasMore, loadingMore, loadMore, upper };\n }\n <\/script>\n <script state>__DASHBOARD_STATE__<\/script>\n </template>\n\n <script type=\"module\">import '@li3/web';<\/script>\n <script src=\"https://cdn.tailwindcss.com\"><\/script>\n</body>\n</html>\n";
|
|
3658
|
+
}, Dn = "<!DOCTYPE html>\n<html lang=\"en\" class=\"dark\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Workflow Engine Dashboard</title>\n <script type=\"importmap\">\n { \"imports\": { \"@li3/\": \"https://cdn.li3.dev/@li3/\" } }\n <\/script>\n</head>\n<body class=\"bg-gray-950 text-gray-100 min-h-screen p-4 md:p-6 font-sans\">\n <template app>\n <main class=\"max-w-6xl mx-auto space-y-6\">\n <header class=\"flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-gray-800 pb-4\">\n <div>\n <h1 class=\"text-2xl font-bold text-white\">Runner Engine Status</h1>\n <p class=\"text-xs text-gray-400\">Live Job Queue & Execution Traces</p>\n </div>\n <div class=\"flex items-center gap-3 text-xs font-mono\">\n <a href=\"/workflows\" class=\"text-indigo-400 hover:text-indigo-300 hover:underline\">Manage workflows</a>\n <span class=\"text-gray-500\">Updated {{ lastUpdated }}</span>\n <span\n class=\"px-3 py-1 rounded-full border\"\n bind-class=\"{\n 'bg-emerald-500/10.text-emerald-400.border-emerald-500/20': !refreshError,\n 'bg-rose-500/10.text-rose-400.border-rose-500/20': refreshError\n }\"\n >\n {{ refreshError ? 'Refresh failed' : 'System operational' }}\n </span>\n </div>\n </header>\n\n <section class=\"bg-gray-900 border border-gray-800 rounded-xl overflow-hidden hidden md:block\">\n <table class=\"w-full text-left text-sm\">\n <thead class=\"bg-gray-800/50 text-gray-400 text-xs uppercase font-mono border-b border-gray-800\">\n <tr>\n <th class=\"py-3 px-4\">Job ID</th>\n <th class=\"py-3 px-4\">Workflow</th>\n <th class=\"py-3 px-4\">Status</th>\n <th class=\"py-3 px-4 hidden xl:table-cell\">Worker</th>\n <th class=\"py-3 px-4 hidden lg:table-cell\">Created At</th>\n <th class=\"py-3 px-4 text-right\">Action</th>\n </tr>\n </thead>\n <tbody>\n <template for=\"job of jobs\">\n <tr class=\"border-b border-gray-800 hover:bg-gray-800/40 transition\">\n <td class=\"py-3 px-4 font-mono text-indigo-400\">\n <a attr-href=\"'/runs/' + job.id\" class=\"hover:underline\">#{{ job.id }}</a>\n </td>\n <td class=\"py-3 px-4 font-medium text-white\">{{ job.workflowId }}</td>\n <td class=\"py-3 px-4\">\n <span\n class=\"px-2.5 py-0.5 rounded-full text-xs font-semibold\"\n bind-class=\"{\n 'bg-emerald-500/10.text-emerald-400': job.status === 'success',\n 'bg-rose-500/10.text-rose-400': job.status === 'failed',\n 'bg-indigo-500/10.text-indigo-400.animate-pulse': job.status === 'running',\n 'bg-gray-500/10.text-gray-400': !['success', 'failed', 'running'].includes(job.status)\n }\"\n >\n {{ upper(job.status) }}\n </span>\n </td>\n <td class=\"py-3 px-4 hidden xl:table-cell text-xs font-mono text-gray-400\">{{ job.workerId || '-' }}</td>\n <td class=\"py-3 px-4 hidden lg:table-cell text-xs text-gray-400\">{{ job.createdAt }}</td>\n <td class=\"py-3 px-4 text-right whitespace-nowrap\">\n <a attr-href=\"'/runs/' + job.id\" class=\"text-xs bg-gray-800 hover:bg-gray-700 text-gray-200 px-3 py-1 rounded border border-gray-700\">View Trace</a>\n </td>\n </tr>\n </template>\n </tbody>\n </table>\n <template if=\"!jobs.length\">\n <div class=\"p-8 text-center text-gray-500\">No jobs recorded yet.</div>\n </template>\n </section>\n\n <section class=\"md:hidden bg-gray-900 border border-gray-800 rounded-xl divide-y divide-gray-800 overflow-hidden\">\n <template for=\"job of jobs\">\n <article class=\"p-4 bg-gray-900/60\">\n <div class=\"flex items-start justify-between gap-3\">\n <div class=\"min-w-0\">\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0\"\n bind-class=\"{\n 'bg-emerald-400': job.status === 'success',\n 'bg-rose-400': job.status === 'failed',\n 'bg-indigo-400.animate-pulse': job.status === 'running',\n 'bg-gray-500': !['success', 'failed', 'running'].includes(job.status)\n }\"\n ></span>\n <h2 class=\"font-medium text-white truncate\">{{ job.workflowId }}</h2>\n </div>\n <p class=\"mt-1 pl-[18px] text-xs font-mono text-gray-400\">#{{ job.id }} ยท {{ upper(job.status) }}</p>\n </div>\n <a attr-href=\"'/runs/' + job.id\" class=\"shrink-0 text-xs bg-gray-800 hover:bg-gray-700 text-gray-300 px-2.5 py-1 rounded border border-gray-700\">Trace</a>\n </div>\n </article>\n </template>\n <template if=\"!jobs.length\">\n <div class=\"p-8 text-center text-gray-500\">No jobs recorded yet.</div>\n </template>\n </section>\n\n <template if=\"hasMore\">\n <div class=\"flex justify-center\">\n <button\n class=\"px-4 py-2 text-sm text-gray-200 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg\"\n on-click=\"loadMore()\"\n >\n {{ loadingMore ? 'Loading...' : 'Load more' }}\n </button>\n </div>\n </template>\n </main>\n\n <script setup>\n import { onDestroy, onInit, ref } from '@li3/web';\n\n export default function setup() {\n const jobs = ref([]);\n const lastUpdated = ref('now');\n const refreshError = ref(false);\n const hasMore = ref(false);\n const loadingMore = ref(false);\n let refreshTimer;\n let eventSource;\n let refreshing = false;\n let refreshPending = false;\n\n const upper = (value) => String(value || '').toUpperCase();\n const terminalStatuses = new Set(['success', 'failed', 'cancelled']);\n const refreshJobs = async () => {\n if (refreshing) {\n refreshPending = true;\n return;\n }\n refreshing = true;\n\n try {\n const activeIds = jobs.value\n .filter((job) => !terminalStatuses.has(job.status))\n .map((job) => Number(job.id));\n const knownIds = jobs.value.map((job) => Number(job.id));\n const afterId = activeIds.length\n ? Math.max(0, Math.min(...activeIds) - 1)\n : knownIds.length\n ? Math.max(...knownIds)\n : null;\n const url = afterId === null ? '/api/jobs?limit=500' : `/api/jobs?afterId=${afterId}&limit=500`;\n const response = await fetch(url, { headers: { accept: 'application/json' } });\n if (!response.ok) throw new Error(`Dashboard refresh failed: ${response.status}`);\n\n const data = await response.json();\n const merged = new Map(jobs.value.map((job) => [job.id, job]));\n for (const job of data.jobs) merged.set(job.id, job);\n jobs.value = Array.from(merged.values()).sort((a, b) => Number(b.id) - Number(a.id));\n lastUpdated.value = new Date().toLocaleTimeString();\n refreshError.value = false;\n } catch (error) {\n console.error(error);\n refreshError.value = true;\n } finally {\n refreshing = false;\n if (refreshPending) {\n refreshPending = false;\n void refreshJobs();\n }\n }\n };\n\n const loadMore = async () => {\n if (loadingMore.value || !hasMore.value || !jobs.value.length) return;\n loadingMore.value = true;\n\n try {\n const beforeId = Math.min(...jobs.value.map((job) => Number(job.id)));\n const response = await fetch(`/api/jobs?beforeId=${beforeId}`, {\n headers: { accept: 'application/json' },\n });\n if (!response.ok) throw new Error(`Loading older jobs failed: ${response.status}`);\n\n const data = await response.json();\n const merged = new Map(jobs.value.map((job) => [job.id, job]));\n for (const job of data.jobs) merged.set(job.id, job);\n jobs.value = Array.from(merged.values()).sort((a, b) => Number(b.id) - Number(a.id));\n hasMore.value = data.hasMore;\n } catch (error) {\n console.error(error);\n refreshError.value = true;\n } finally {\n loadingMore.value = false;\n }\n };\n\n onInit(() => {\n eventSource = new EventSource('/api/events');\n eventSource.addEventListener('jobs.available', refreshJobs);\n eventSource.addEventListener('jobs.changed', refreshJobs);\n eventSource.onerror = () => {\n refreshError.value = true;\n };\n refreshTimer = setInterval(refreshJobs, 60000);\n });\n onDestroy(() => {\n eventSource?.close();\n clearInterval(refreshTimer);\n });\n\n return { jobs, lastUpdated, refreshError, hasMore, loadingMore, loadMore, upper };\n }\n <\/script>\n <script state>__DASHBOARD_STATE__<\/script>\n </template>\n\n <script type=\"module\">import '@li3/web';<\/script>\n <script src=\"https://cdn.tailwindcss.com\"><\/script>\n</body>\n</html>\n";
|
|
3655
3659
|
//#endregion
|
|
3656
3660
|
//#region src/html-state.ts
|
|
3657
3661
|
function On(e) {
|
|
@@ -3732,7 +3736,7 @@ async function Mn(e, t, n, r) {
|
|
|
3732
3736
|
}
|
|
3733
3737
|
//#endregion
|
|
3734
3738
|
//#region src/run.html?raw
|
|
3735
|
-
var Nn = "<!DOCTYPE html>\n<html lang=\"en\" class=\"dark\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Workflow Run</title>\n <script type=\"importmap\">\n {\n \"imports\": {\n \"@li3/\": \"https://cdn.li3.dev/@li3/\",\n \"ansi_up\": \"https://esm.sh/ansi_up@6.0.6\"\n }\n }\n <\/script>\n</head>\n<body class=\"bg-gray-950 text-gray-100 min-h-screen p-4 md:p-6 font-sans\">\n <template app>\n <div class=\"max-w-5xl mx-auto space-y-4\">\n <header class=\"flex flex-col md:flex-row md:items-center justify-between border-b border-gray-800 pb-6 gap-4\">\n <div class=\"min-w-0\">\n <a href=\"/runs\" class=\"text-xs text-indigo-400 hover:underline mb-1 inline-block\">Back to Dashboard</a>\n <h1 class=\"text-2xl font-bold text-white flex flex-wrap items-center gap-3\">\n {{ report.workflowName }}\n <span class=\"text-sm font-mono text-gray-500\">#{{ report.jobId }}</span>\n <template if=\"report.parentId\">\n <a attr-href=\"'/runs/' + report.parentId\" class=\"text-sm font-mono text-indigo-400 hover:underline\">\n from #{{ report.parentId }}\n </a>\n </template>\n </h1>\n <p class=\"text-xs text-gray-400 mt-1\">Started {{ report.startedAt }} ยท {{ timing }}</p>\n </div>\n <div class=\"flex items-center gap-2 shrink-0\">\n <span\n class=\"px-4 py-1.5 rounded-full text-sm font-semibold border\"\n bind-class=\"{\n 'bg-emerald-500/10.text-emerald-400.border-emerald-500/20': report.status === 'success',\n 'bg-rose-500/10.text-rose-400.border-rose-500/20': report.status === 'failed',\n 'bg-indigo-500/10.text-indigo-400.border-indigo-500/20': report.status === 'running',\n 'bg-amber-500/10.text-amber-400.border-amber-500/20': !['success', 'failed', 'running'].includes(report.status)\n }\"\n >\n {{ upper(report.status) }}\n </span>\n <button\n class=\"text-sm text-white px-4 py-1.5 border border-gray-600 hover:border-gray-400 rounded-full\"\n
|
|
3739
|
+
var Nn = "<!DOCTYPE html>\n<html lang=\"en\" class=\"dark\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Workflow Run</title>\n <script type=\"importmap\">\n {\n \"imports\": {\n \"@li3/\": \"https://cdn.li3.dev/@li3/\",\n \"ansi_up\": \"https://esm.sh/ansi_up@6.0.6\"\n }\n }\n <\/script>\n</head>\n<body class=\"bg-gray-950 text-gray-100 min-h-screen p-4 md:p-6 font-sans\">\n <template app>\n <div class=\"max-w-5xl mx-auto space-y-4\">\n <header class=\"flex flex-col md:flex-row md:items-center justify-between border-b border-gray-800 pb-6 gap-4\">\n <div class=\"min-w-0\">\n <a href=\"/runs\" class=\"text-xs text-indigo-400 hover:underline mb-1 inline-block\">Back to Dashboard</a>\n <h1 class=\"text-2xl font-bold text-white flex flex-wrap items-center gap-3\">\n {{ report.workflowName }}\n <span class=\"text-sm font-mono text-gray-500\">#{{ report.jobId }}</span>\n <template if=\"report.parentId\">\n <a attr-href=\"'/runs/' + report.parentId\" class=\"text-sm font-mono text-indigo-400 hover:underline\">\n from #{{ report.parentId }}\n </a>\n </template>\n </h1>\n <p class=\"text-xs text-gray-400 mt-1\">Started {{ report.startedAt }} ยท {{ timing }}</p>\n </div>\n <div class=\"flex items-center gap-2 shrink-0\">\n <span\n class=\"px-4 py-1.5 rounded-full text-sm font-semibold border\"\n bind-class=\"{\n 'bg-emerald-500/10.text-emerald-400.border-emerald-500/20': report.status === 'success',\n 'bg-rose-500/10.text-rose-400.border-rose-500/20': report.status === 'failed',\n 'bg-indigo-500/10.text-indigo-400.border-indigo-500/20': report.status === 'running',\n 'bg-amber-500/10.text-amber-400.border-amber-500/20': !['success', 'failed', 'running'].includes(report.status)\n }\"\n >\n {{ upper(report.status) }}\n </span>\n <template if=\"report.status === 'running'\">\n <button\n class=\"text-sm text-rose-300 px-4 py-1.5 border border-rose-500/50 hover:border-rose-400 rounded-full disabled:opacity-50\"\n on-click=\"cancelJob()\"\n attr-disabled=\"cancelling\"\n >\n {{ cancelling ? 'Cancelling...' : 'Cancel' }}\n </button>\n </template>\n <template if=\"report.status !== 'running'\">\n <button\n class=\"text-sm text-white px-4 py-1.5 border border-gray-600 hover:border-gray-400 rounded-full\"\n on-click=\"restartJob()\"\n >\n Restart\n </button>\n </template>\n </div>\n </header>\n\n <section class=\"rounded overflow-hidden border-b border-gray-800\">\n <h2 class=\"sr-only\">Execution Steps</h2>\n <template for=\"[step, index] of report.steps\">\n <details class=\"border border-b-0 border-gray-800 bg-gray-900/50 text-sm\">\n <summary class=\"flex flex-col md:flex-row md:items-center justify-between gap-2 p-3 bg-gray-800/40 border-b border-gray-800 cursor-pointer\">\n <div class=\"flex flex-wrap items-center gap-3 min-w-0\">\n <span class=\"font-mono text-gray-500\">\n #{{ index + 1 }}<template if=\"step.exitCode !== undefined && step.exitCode !== 0\"> ({{ step.exitCode }})</template>\n </span>\n <h3 class=\"font-semibold text-gray-200 break-words\">{{ step.name }}</h3>\n <span\n class=\"px-2.5 py-0.5 rounded-full font-medium\"\n bind-class=\"{\n 'text-emerald-400.bg-emerald-500/10': step.status === 'success',\n 'text-rose-400.bg-rose-500/10': step.status === 'failed',\n 'text-indigo-400.bg-indigo-500/10': step.status === 'running',\n 'text-gray-400.bg-gray-500/10': !['success', 'failed', 'running'].includes(step.status)\n }\"\n >\n {{ upper(step.status) }}\n </span>\n </div>\n <div class=\"font-mono text-gray-400 shrink-0\">{{ step.durationMs }}ms</div>\n </summary>\n <div class=\"p-4 bg-gray-950 font-mono text-gray-300 leading-relaxed overflow-auto w-full\">\n <pre class=\"whitespace-pre-wrap\" bind-innerhtml=\"stepLog(step)\"></pre>\n </div>\n </details>\n </template>\n </section>\n\n <details class=\"bg-gray-900 border border-gray-800 rounded-xl p-4 overflow-auto max-h-[400px]\">\n <summary class=\"cursor-pointer\">\n <h2 class=\"inline text-xs font-semibold text-gray-400 uppercase tracking-wider\">Trigger Inputs</h2>\n </summary>\n <pre class=\"font-mono text-xs text-gray-200 bg-gray-950 p-3 mt-3 rounded-lg overflow-x-auto\">{{ inputsJson }}</pre>\n </details>\n </div>\n\n <script setup>\n import { computed, onDestroy, onInit, ref } from '@li3/web';\n import { AnsiUp } from 'ansi_up';\n\n export default function setup() {\n const report = ref({});\n const now = ref(Date.now());\n const ansiUp = new AnsiUp();\n ansiUp.use_classes = false;\n let eventSource;\n let refreshTimer;\n let clockTimer;\n let refreshing = false;\n let refreshPending = false;\n const cancelling = ref(false);\n\n const active = computed(() => ['pending', 'running'].includes(report.value.status));\n const inputsJson = computed(() => JSON.stringify(report.value.inputs || {}, null, 2));\n const timing = computed(() => {\n if (report.value.status === 'pending') return 'Waiting for a worker';\n if (report.value.status === 'running') {\n return `Running for ${Math.max(0, now.value - Date.parse(report.value.startedAt))}ms`;\n }\n return `Finished in ${report.value.durationMs}ms`;\n });\n\n const upper = (value) => String(value || '').toUpperCase();\n const stepLog = (step) => {\n if (step.status === 'skipped') return '';\n if (step.status === 'running') return '<span class=\"text-indigo-400\">Step is running. Logs will appear after it finishes.</span>';\n if (step.status === 'pending') return '<span class=\"text-gray-500\">Waiting to run.</span>';\n if (step.logContent) return ansiUp.ansi_to_html(step.logContent);\n return '<span class=\"text-gray-500\">(No terminal log output recorded for this step)</span>';\n };\n const restartJob = async () => {\n const response = await fetch(`/restart/${report.value.jobId}`, { method: 'POST' });\n if (response.ok) {\n const { id } = await response.json();\n location.href = `/runs/${id}`;\n }\n };\n const cancelJob = async () => {\n if (cancelling.value || report.value.status !== 'running') return;\n cancelling.value = true;\n try {\n const response = await fetch(`/api/jobs/${report.value.jobId}/cancel`, { method: 'POST' });\n if (!response.ok) throw new Error(`Cancel failed: ${response.status}`);\n await refreshRun();\n } catch (error) {\n console.error(error);\n } finally {\n cancelling.value = false;\n }\n };\n const refreshRun = async () => {\n if (refreshing) {\n refreshPending = true;\n return;\n }\n refreshing = true;\n\n try {\n const response = await fetch(`/api/runs/${report.value.jobId}`, {\n headers: { accept: 'application/json' },\n });\n if (!response.ok) throw new Error(`Run refresh failed: ${response.status}`);\n report.value = await response.json();\n } catch (error) {\n console.error(error);\n } finally {\n refreshing = false;\n if (refreshPending) {\n refreshPending = false;\n void refreshRun();\n }\n }\n };\n const handleJobChange = (event) => {\n const data = JSON.parse(event.data || '{}');\n if (!data.jobId || String(data.jobId) === String(report.value.jobId)) void refreshRun();\n };\n\n onInit(() => {\n document.title = `Run #${report.value.jobId} - ${report.value.workflowName}`;\n eventSource = new EventSource('/api/events');\n eventSource.addEventListener('jobs.changed', handleJobChange);\n refreshTimer = setInterval(() => {\n if (active.value) void refreshRun();\n }, 60000);\n clockTimer = setInterval(() => {\n now.value = Date.now();\n }, 1000);\n });\n onDestroy(() => {\n eventSource?.close();\n clearInterval(refreshTimer);\n clearInterval(clockTimer);\n });\n\n return { report, inputsJson, timing, upper, stepLog, restartJob, cancelJob, cancelling };\n }\n <\/script>\n <script state>__REPORT_STATE__<\/script>\n </template>\n\n <script type=\"module\">import '@li3/web';<\/script>\n <script src=\"https://cdn.tailwindcss.com\"><\/script>\n</body>\n</html>\n", Pn = /(?:^|[_-])auth(?:entication)?(?:$|[_-])|access[_-]?key|api[_-]?key|authorization|cookie|credential|passphrase|password|private[_-]?key|secret|session(?:id)?|signing[_-]?key|token/i;
|
|
3736
3740
|
function Fn(e, t, n, r = []) {
|
|
3737
3741
|
let i = JSON.parse(e.payload), a = e.report ? JSON.parse(e.report) : Ln(e, i, r), o = e.status;
|
|
3738
3742
|
return {
|
|
@@ -8244,11 +8248,23 @@ var Ho = class {
|
|
|
8244
8248
|
async names() {
|
|
8245
8249
|
return (await S.all("SELECT name FROM secrets ORDER BY name")).map((e) => e.name);
|
|
8246
8250
|
}
|
|
8251
|
+
async delete(e) {
|
|
8252
|
+
let t = await S.run("DELETE FROM secrets WHERE name = ?", [e]);
|
|
8253
|
+
return Number(t?.changes || 0) > 0;
|
|
8254
|
+
}
|
|
8247
8255
|
decrypt(e) {
|
|
8248
8256
|
let t = Buffer.from(e, "base64"), n = l.createDecipheriv("aes-256-gcm", this.encryptionKey(), t.subarray(0, 12));
|
|
8249
8257
|
return n.setAuthTag(t.subarray(12, 28)), Buffer.concat([n.update(t.subarray(28)), n.final()]).toString("utf8");
|
|
8250
8258
|
}
|
|
8251
|
-
}, Uo = 50, Wo = 500, Go = class e {
|
|
8259
|
+
}, Uo = "<!DOCTYPE html>\n<html lang=\"en\" class=\"dark\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Runner Workflow Management</title>\n <script src=\"https://cdn.tailwindcss.com\"><\/script>\n <style>\n body[data-page=\"list\"] .editor-panel { display: none; }\n body[data-page=\"editor\"] .workflow-list, body[data-page=\"editor\"] .secrets-panel { display: none; }\n body[data-page=\"list\"] #workflow-layout { grid-template-columns: minmax(0, 1fr); }\n </style>\n</head>\n<body data-page=\"__WORKFLOW_PAGE__\" class=\"min-h-screen bg-gray-950 p-4 font-sans text-gray-100 md:p-6\">\n <main class=\"mx-auto max-w-7xl space-y-5\">\n <header class=\"flex flex-col justify-between gap-3 border-b border-gray-800 pb-4 sm:flex-row sm:items-end\">\n <div>\n <a href=\"/runs\" class=\"text-xs text-indigo-400 hover:underline\">Back to dashboard</a>\n <h1 class=\"mt-1 text-2xl font-bold text-white\">Workflow control room</h1>\n <p class=\"text-sm text-gray-400\">Draft, validate, and publish revisioned YAML definitions.</p>\n </div>\n <a href=\"/workflows/new\" class=\"rounded-lg border border-indigo-500/50 bg-indigo-500/10 px-4 py-2 text-sm font-medium text-indigo-200 hover:bg-indigo-500/20\">New workflow</a>\n </header>\n\n <p id=\"notice\" role=\"status\" class=\"hidden rounded-lg border px-3 py-2 text-sm\"></p>\n\n <section id=\"workflow-layout\" class=\"grid gap-5 lg:grid-cols-[17rem_1fr]\">\n <aside class=\"workflow-list rounded-xl border border-gray-800 bg-gray-900 p-3\">\n <h2 class=\"mb-2 px-1 text-xs font-semibold uppercase tracking-wider text-gray-400\">Workflows</h2>\n <div id=\"workflow-list\" class=\"space-y-1\"></div>\n </aside>\n\n <section class=\"editor-panel rounded-xl border border-gray-800 bg-gray-900 p-4\">\n <div class=\"mb-3 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between\">\n <label class=\"block flex-1 text-xs font-semibold uppercase tracking-wider text-gray-400\">Workflow ID\n <input id=\"workflow-id\" class=\"mt-1 w-full rounded border border-gray-700 bg-gray-950 px-3 py-2 font-mono text-sm text-white outline-none focus:border-indigo-500\" placeholder=\"derived-from-workflow-name\">\n </label>\n <div class=\"flex flex-wrap gap-2\">\n <button id=\"validate\" class=\"rounded border border-gray-700 px-3 py-2 text-sm hover:border-gray-500\">Validate</button>\n <button id=\"save\" class=\"rounded border border-indigo-500 bg-indigo-500 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-400\">Save draft</button>\n <button id=\"publish\" class=\"rounded border border-emerald-500/60 px-3 py-2 text-sm text-emerald-300 hover:bg-emerald-500/10\">Publish</button>\n <button id=\"delete\" class=\"rounded border border-rose-500/60 px-3 py-2 text-sm text-rose-300 hover:bg-rose-500/10\">Delete</button>\n </div>\n </div>\n <textarea id=\"source-yaml\" spellcheck=\"false\" class=\"h-[34rem] w-full rounded-lg border border-gray-700 bg-gray-950 p-4 font-mono text-sm leading-6 text-gray-100 outline-none focus:border-indigo-500\" placeholder=\"name: Example on: github: events: [push] steps: - run: echo hello\"></textarea>\n </section>\n </section>\n\n <section class=\"secrets-panel rounded-xl border border-gray-800 bg-gray-900 p-4\">\n <div class=\"mb-3\">\n <h2 class=\"text-base font-semibold text-white\">Secrets</h2>\n <p class=\"text-sm text-gray-400\">Values are write-only and encrypted. Re-saving a name replaces its value.</p>\n </div>\n <div class=\"grid gap-3 md:grid-cols-[14rem_1fr_auto_auto]\">\n <input id=\"secret-name\" class=\"rounded border border-gray-700 bg-gray-950 px-3 py-2 font-mono text-sm uppercase outline-none focus:border-indigo-500\" placeholder=\"NPM_TOKEN\">\n <input id=\"secret-value\" type=\"password\" class=\"rounded border border-gray-700 bg-gray-950 px-3 py-2 font-mono text-sm outline-none focus:border-indigo-500\" placeholder=\"New secret value\">\n <button id=\"save-secret\" class=\"rounded border border-indigo-500 px-3 py-2 text-sm text-indigo-200 hover:bg-indigo-500/10\">Save value</button>\n <button id=\"delete-secret\" class=\"rounded border border-rose-500/60 px-3 py-2 text-sm text-rose-300 hover:bg-rose-500/10\">Delete</button>\n </div>\n <div id=\"secret-list\" class=\"mt-4 flex flex-wrap gap-2\"></div>\n </section>\n </main>\n\n <script>\n const notice = document.querySelector('#notice');\n const workflowList = document.querySelector('#workflow-list');\n const source = document.querySelector('#source-yaml');\n const workflowId = document.querySelector('#workflow-id');\n const secretList = document.querySelector('#secret-list');\n const secretName = document.querySelector('#secret-name');\n const secretValue = document.querySelector('#secret-value');\n const page = document.body.dataset.page;\n const initialId = __WORKFLOW_ID__;\n let workflows = [];\n let selectedId = '';\n\n const showNotice = (message, error = false) => {\n notice.textContent = message;\n notice.className = `rounded-lg border px-3 py-2 text-sm ${error ? 'border-rose-500/40 bg-rose-500/10 text-rose-200' : 'border-emerald-500/40 bg-emerald-500/10 text-emerald-200'}`;\n };\n const api = async (url, options = {}) => {\n const response = await fetch(url, { headers: { accept: 'application/json', ...(options.body ? { 'content-type': 'application/json' } : {}) }, ...options });\n const body = response.status === 204 ? null : await response.json().catch(() => ({}));\n if (!response.ok) throw new Error(body.error || `Request failed: ${response.status}`);\n return body;\n };\n const derivedId = () => source.value.match(/^\\s*(?:id:\\s*([^\\n]+)|name:\\s*([^\\n]+))/m)?.slice(1).find(Boolean)?.trim().toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/(^-|-$)/g, '') || '';\n const renderWorkflows = () => {\n workflowList.replaceChildren();\n for (const workflow of workflows) {\n const button = document.createElement('button');\n button.className = `block w-full rounded px-3 py-2 text-left text-sm hover:bg-gray-800 ${workflow.id === selectedId ? 'bg-indigo-500/15 text-indigo-200' : 'text-gray-300'}`;\n button.textContent = `${workflow.name} ยท ${workflow.status}`;\n button.onclick = () => { window.location.href = `/workflows/${workflow.id}`; };\n workflowList.append(button);\n }\n };\n const loadWorkflows = async () => {\n workflows = (await api('/api/workflows')).workflows;\n renderWorkflows();\n };\n const openWorkflow = async (id) => {\n const workflow = await api(`/api/workflows/${id}`);\n selectedId = workflow.id;\n workflowId.value = workflow.id;\n source.value = workflow.sourceYaml;\n renderWorkflows();\n };\n const loadSecrets = async () => {\n const secrets = (await api('/api/secrets')).secrets;\n secretList.replaceChildren();\n for (const name of secrets) {\n const button = document.createElement('button');\n button.className = 'rounded border border-gray-700 px-2 py-1 font-mono text-xs text-gray-300 hover:border-indigo-500';\n button.textContent = name;\n button.onclick = () => { secretName.value = name; secretValue.focus(); };\n secretList.append(button);\n }\n };\n document.querySelector('#validate').onclick = async () => {\n try { const result = await api('/api/workflows/validate', { method: 'POST', body: JSON.stringify({ sourceYaml: source.value }) }); showNotice(`Valid: ${result.workflows.length} workflow definition(s).`); } catch (error) { showNotice(error.message, true); }\n };\n document.querySelector('#save').onclick = async () => {\n const id = workflowId.value.trim() || derivedId();\n if (!id) return showNotice('Set an ID or add a workflow name first.', true);\n try { const workflow = await api(`/api/workflows/${id}`, { method: 'PUT', body: JSON.stringify({ sourceYaml: source.value }) }); selectedId = workflow.id; workflowId.value = workflow.id; showNotice(`Saved ${workflow.id} as draft revision ${workflow.revision}.`); if (!initialId) history.replaceState(null, '', `/workflows/${workflow.id}`); } catch (error) { showNotice(error.message, true); }\n };\n document.querySelector('#publish').onclick = async () => {\n const id = workflowId.value.trim() || selectedId;\n if (!id) return showNotice('Save a workflow before publishing it.', true);\n try { const workflow = await api(`/api/workflows/${id}/publish`, { method: 'POST' }); selectedId = workflow.id; showNotice(`Published ${workflow.id} revision ${workflow.revision}.`); } catch (error) { showNotice(error.message, true); }\n };\n document.querySelector('#delete').onclick = async () => {\n const id = workflowId.value.trim() || selectedId;\n if (!id || !confirm(`Delete ${id} and all of its revisions?`)) return;\n try { await api(`/api/workflows/${id}`, { method: 'DELETE' }); window.location.href = '/workflows'; } catch (error) { showNotice(error.message, true); }\n };\n document.querySelector('#save-secret').onclick = async () => {\n try { await api(`/api/secrets/${secretName.value.trim()}`, { method: 'PUT', body: JSON.stringify({ value: secretValue.value }) }); secretValue.value = ''; await loadSecrets(); showNotice(`Saved ${secretName.value.trim()}.`); } catch (error) { showNotice(error.message, true); }\n };\n document.querySelector('#delete-secret').onclick = async () => {\n const name = secretName.value.trim();\n if (!name || !confirm(`Delete ${name}?`)) return;\n try { await api(`/api/secrets/${name}`, { method: 'DELETE' }); secretName.value = ''; secretValue.value = ''; await loadSecrets(); showNotice(`Deleted ${name}.`); } catch (error) { showNotice(error.message, true); }\n };\n const loadPage = page === 'editor'\n ? (initialId ? openWorkflow(initialId) : Promise.resolve())\n : Promise.all([loadWorkflows(), loadSecrets()]);\n loadPage.catch((error) => showNotice(error.message, true));\n <\/script>\n</body>\n</html>\n";
|
|
8260
|
+
//#endregion
|
|
8261
|
+
//#region src/workflows-ui.ts
|
|
8262
|
+
function Wo(e, t = "") {
|
|
8263
|
+
return Uo.replace("__WORKFLOW_PAGE__", e).replace("__WORKFLOW_ID__", JSON.stringify(t));
|
|
8264
|
+
}
|
|
8265
|
+
//#endregion
|
|
8266
|
+
//#region src/server.ts
|
|
8267
|
+
var Go = 50, Ko = 500, qo = class e {
|
|
8252
8268
|
server;
|
|
8253
8269
|
preprocessors = /* @__PURE__ */ new Map();
|
|
8254
8270
|
workflows = new Bo();
|
|
@@ -8272,21 +8288,37 @@ var Ho = class {
|
|
|
8272
8288
|
async handleRequest(e, t) {
|
|
8273
8289
|
let n = new u(e.url || "/", `${e.headers["x-forwarded-proto"] || "http"}://${e.headers["x-forwarded-host"] || e.headers.host}`);
|
|
8274
8290
|
if (e.method === "GET" && (n.pathname === "/runs" || n.pathname === "/")) return this.renderDashboard(t);
|
|
8291
|
+
if (e.method === "GET" && n.pathname === "/workflows") return this.requireAdmin(e, t) ? (t.writeHead(200, {
|
|
8292
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
8293
|
+
"Cache-Control": "no-store"
|
|
8294
|
+
}), t.end(Wo("list"))) : void 0;
|
|
8295
|
+
let r = n.pathname.match(/^\/workflows\/(new|[a-z0-9-]+)$/);
|
|
8296
|
+
if (e.method === "GET" && r) {
|
|
8297
|
+
if (!this.requireAdmin(e, t)) return;
|
|
8298
|
+
let n = r[1] === "new" ? "" : r[1];
|
|
8299
|
+
return t.writeHead(200, {
|
|
8300
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
8301
|
+
"Cache-Control": "no-store"
|
|
8302
|
+
}), t.end(Wo("editor", n));
|
|
8303
|
+
}
|
|
8275
8304
|
if (e.method === "GET" && n.pathname === "/api/jobs") {
|
|
8276
|
-
let e = n.searchParams.get("afterId"), r = n.searchParams.get("beforeId"), i = n.searchParams.get("limit"), a = e === null ? void 0 : Number(e), o = r === null ? void 0 : Number(r), s = i === null ?
|
|
8277
|
-
return a !== void 0 && (!Number.isSafeInteger(a) || a < 0) ? (t.writeHead(400, { "Content-Type": "application/json; charset=utf-8" }), t.end(JSON.stringify({ error: "afterId must be a non-negative integer" }))) : o !== void 0 && (!Number.isSafeInteger(o) || o < 1) ? (t.writeHead(400, { "Content-Type": "application/json; charset=utf-8" }), t.end(JSON.stringify({ error: "beforeId must be a positive integer" }))) : !Number.isSafeInteger(s) || s < 1 || s >
|
|
8305
|
+
let e = n.searchParams.get("afterId"), r = n.searchParams.get("beforeId"), i = n.searchParams.get("limit"), a = e === null ? void 0 : Number(e), o = r === null ? void 0 : Number(r), s = i === null ? Go : Number(i);
|
|
8306
|
+
return a !== void 0 && (!Number.isSafeInteger(a) || a < 0) ? (t.writeHead(400, { "Content-Type": "application/json; charset=utf-8" }), t.end(JSON.stringify({ error: "afterId must be a non-negative integer" }))) : o !== void 0 && (!Number.isSafeInteger(o) || o < 1) ? (t.writeHead(400, { "Content-Type": "application/json; charset=utf-8" }), t.end(JSON.stringify({ error: "beforeId must be a positive integer" }))) : !Number.isSafeInteger(s) || s < 1 || s > Ko ? (t.writeHead(400, { "Content-Type": "application/json; charset=utf-8" }), t.end(JSON.stringify({ error: `limit must be an integer from 1 to ${Ko}` }))) : this.renderDashboardJobs(t, s, a, o);
|
|
8278
8307
|
}
|
|
8279
8308
|
if (e.method === "GET" && n.pathname === "/api/events") return this.events.subscribe(e, t);
|
|
8280
8309
|
if (n.pathname === "/api/workflows/validate" && e.method === "POST") return this.handleWorkflowValidation(e, t);
|
|
8281
8310
|
if (n.pathname === "/api/secrets" && e.method === "GET") return this.handleSecretList(e, t);
|
|
8282
|
-
let
|
|
8283
|
-
if (
|
|
8284
|
-
|
|
8285
|
-
|
|
8311
|
+
let i = n.pathname.match(/^\/api\/secrets\/([A-Z][A-Z0-9_]*)$/);
|
|
8312
|
+
if (i && e.method === "PUT") return this.handleSecretSave(e, t, i[1]);
|
|
8313
|
+
if (i && e.method === "DELETE") return this.handleSecretDelete(e, t, i[1]);
|
|
8314
|
+
let a = n.pathname.match(/^\/api\/jobs\/(\d+)\/secrets$/);
|
|
8315
|
+
if (a && e.method === "GET") return this.handleJobSecrets(e, t, a[1]);
|
|
8316
|
+
let o = n.pathname.match(/^\/api\/jobs\/(\d+)\/cancel$/);
|
|
8317
|
+
if (o && e.method === "POST") return this.handleCancelJob(e, t, o[1]);
|
|
8286
8318
|
if (n.pathname === "/api/workflows" && e.method === "GET") return this.handleWorkflowList(e, t);
|
|
8287
|
-
let
|
|
8288
|
-
if (
|
|
8289
|
-
let [, n, r] =
|
|
8319
|
+
let s = n.pathname.match(/^\/api\/workflows\/([a-z0-9-]+)(\/publish)?$/);
|
|
8320
|
+
if (s) {
|
|
8321
|
+
let [, n, r] = s;
|
|
8290
8322
|
if (e.method === "GET" && !r) return this.handleWorkflowGet(e, t, n);
|
|
8291
8323
|
if (e.method === "PUT" && !r) return this.handleWorkflowSave(e, t, n);
|
|
8292
8324
|
if (e.method === "DELETE" && !r) return this.handleWorkflowDelete(e, t, n);
|
|
@@ -8498,6 +8530,12 @@ var Ho = class {
|
|
|
8498
8530
|
t.writeHead(422, { "Content-Type": "application/json" }), t.end(JSON.stringify({ error: e.message }));
|
|
8499
8531
|
}
|
|
8500
8532
|
}
|
|
8533
|
+
async handleSecretDelete(e, t, n) {
|
|
8534
|
+
if (this.requireAdmin(e, t)) {
|
|
8535
|
+
if (!await this.secretRepository.delete(n)) return t.writeHead(404, { "Content-Type": "application/json" }), t.end(JSON.stringify({ error: "Secret not found" }));
|
|
8536
|
+
t.writeHead(204).end();
|
|
8537
|
+
}
|
|
8538
|
+
}
|
|
8501
8539
|
async handleJobSecrets(e, t, n) {
|
|
8502
8540
|
if (!this.isWorker(e)) return t.writeHead(403, { "Content-Type": "application/json" }), t.end(JSON.stringify({ error: "Unauthorized" }));
|
|
8503
8541
|
let r = await this.queue.getJob(n);
|
|
@@ -8525,7 +8563,7 @@ var Ho = class {
|
|
|
8525
8563
|
this.events.publish("jobs.changed", r ? { jobId: r } : {}), t.writeHead(202).end();
|
|
8526
8564
|
}
|
|
8527
8565
|
async renderDashboard(e) {
|
|
8528
|
-
let t = await this.queue.listJobs(51), n = kn(t.slice(0,
|
|
8566
|
+
let t = await this.queue.listJobs(51), n = kn(t.slice(0, Go)), r = await this.redactText(An(n, t.length > Go));
|
|
8529
8567
|
e.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }), e.end(r);
|
|
8530
8568
|
}
|
|
8531
8569
|
async renderDashboardJobs(e, t, n, r) {
|
|
@@ -8560,6 +8598,16 @@ var Ho = class {
|
|
|
8560
8598
|
}
|
|
8561
8599
|
n.writeHead(404, { "Content-Type": "application/json" }), n.end(JSON.stringify({ error: "Job not found" }));
|
|
8562
8600
|
}
|
|
8601
|
+
async handleCancelJob(e, t, n) {
|
|
8602
|
+
if (!this.requireAdmin(e, t)) return;
|
|
8603
|
+
let r = await this.queue.cancelJob(n);
|
|
8604
|
+
if (r === "not_found") return t.writeHead(404, { "Content-Type": "application/json" }), t.end(JSON.stringify({ error: "Job not found" }));
|
|
8605
|
+
if (r === "not_active") return t.writeHead(409, { "Content-Type": "application/json" }), t.end(JSON.stringify({ error: "Job is no longer running" }));
|
|
8606
|
+
this.events.publish("jobs.changed", { jobId: Number(n) }), t.writeHead(200, { "Content-Type": "application/json" }), t.end(JSON.stringify({
|
|
8607
|
+
id: Number(n),
|
|
8608
|
+
status: "cancelled"
|
|
8609
|
+
}));
|
|
8610
|
+
}
|
|
8563
8611
|
async currentSecrets() {
|
|
8564
8612
|
try {
|
|
8565
8613
|
return await this.secretRepository.getAll();
|
|
@@ -8589,7 +8637,7 @@ var Ho = class {
|
|
|
8589
8637
|
});
|
|
8590
8638
|
});
|
|
8591
8639
|
}
|
|
8592
|
-
},
|
|
8640
|
+
}, Jo = s(d), Yo = class {
|
|
8593
8641
|
name = "systemd";
|
|
8594
8642
|
async isSupported() {
|
|
8595
8643
|
try {
|
|
@@ -8712,7 +8760,7 @@ var Ho = class {
|
|
|
8712
8760
|
cancel: async () => {
|
|
8713
8761
|
d = !0;
|
|
8714
8762
|
try {
|
|
8715
|
-
t.image && await
|
|
8763
|
+
t.image && await Jo(`docker stop -t 2 ${o}`).catch(() => {}), await Jo(`systemctl stop ${o}.service`).catch(() => {});
|
|
8716
8764
|
} catch {}
|
|
8717
8765
|
},
|
|
8718
8766
|
logFilePath: a
|
|
@@ -8722,7 +8770,7 @@ var Ho = class {
|
|
|
8722
8770
|
let t = await p(e, "utf-8"), n = t.includes("Running as unit: ") ? t.indexOf("\n") + 1 : 0, r = t.includes("Finished with result: ") ? t.lastIndexOf("Finished with result: ") : t.length;
|
|
8723
8771
|
return t.slice(n, r);
|
|
8724
8772
|
}
|
|
8725
|
-
},
|
|
8773
|
+
}, Xo = class {
|
|
8726
8774
|
name = "standard-process";
|
|
8727
8775
|
async isSupported() {
|
|
8728
8776
|
return !0;
|
|
@@ -8837,13 +8885,13 @@ var Ho = class {
|
|
|
8837
8885
|
};
|
|
8838
8886
|
//#endregion
|
|
8839
8887
|
//#region src/drivers/index.ts
|
|
8840
|
-
async function
|
|
8841
|
-
let e = new
|
|
8842
|
-
return await e.isSupported() ? (console.log("โก Selected Execution Driver: Systemd (cgroups enabled)"), e) : (console.log("๐ฆ Selected Execution Driver: Standard Process (Fallback)"), new
|
|
8888
|
+
async function Zo() {
|
|
8889
|
+
let e = new Yo();
|
|
8890
|
+
return await e.isSupported() ? (console.log("โก Selected Execution Driver: Systemd (cgroups enabled)"), e) : (console.log("๐ฆ Selected Execution Driver: Standard Process (Fallback)"), new Xo());
|
|
8843
8891
|
}
|
|
8844
8892
|
//#endregion
|
|
8845
8893
|
//#region src/signals.ts
|
|
8846
|
-
async function
|
|
8894
|
+
async function Qo(e) {
|
|
8847
8895
|
let t = !1;
|
|
8848
8896
|
async function n(n) {
|
|
8849
8897
|
if (!t) {
|
|
@@ -8851,7 +8899,7 @@ async function Xo(e) {
|
|
|
8851
8899
|
console.error("โ ๏ธ Graceful shutdown timed out after 10s. Forcing exit!"), process.exit(1);
|
|
8852
8900
|
}, 1e4).unref();
|
|
8853
8901
|
try {
|
|
8854
|
-
|
|
8902
|
+
as(), e.length > 0 && (console.log("โ๏ธ Waiting for active worker jobs to drain..."), await Promise.allSettled(e)), console.log("โจ Engine stopped cleanly. Goodbye!"), process.exit(0);
|
|
8855
8903
|
} catch (e) {
|
|
8856
8904
|
console.error("โ Error during graceful shutdown:", e.message), process.exit(1);
|
|
8857
8905
|
}
|
|
@@ -8861,7 +8909,7 @@ async function Xo(e) {
|
|
|
8861
8909
|
}
|
|
8862
8910
|
//#endregion
|
|
8863
8911
|
//#region src/plugins/manager.ts
|
|
8864
|
-
var
|
|
8912
|
+
var $o = class {
|
|
8865
8913
|
plugins;
|
|
8866
8914
|
constructor(e = []) {
|
|
8867
8915
|
this.plugins = e;
|
|
@@ -8883,38 +8931,38 @@ var Zo = class {
|
|
|
8883
8931
|
console.error(`[Plugin Error] ${n.name}.onWorkflowFinish:`, e);
|
|
8884
8932
|
}
|
|
8885
8933
|
}
|
|
8886
|
-
},
|
|
8887
|
-
function
|
|
8888
|
-
|
|
8934
|
+
}, es = !!process.env.DEBUG, ts = { isStopping: !1 }, ns = /* @__PURE__ */ new Set(), rs = null, is = null;
|
|
8935
|
+
function as() {
|
|
8936
|
+
ts.isStopping = !0, rs?.abort(), is?.();
|
|
8889
8937
|
}
|
|
8890
|
-
function
|
|
8891
|
-
|
|
8892
|
-
let i = [
|
|
8893
|
-
return
|
|
8938
|
+
function os(e, t, n, r) {
|
|
8939
|
+
ts.isStopping = !1;
|
|
8940
|
+
let i = [ss(e, t, n, r)];
|
|
8941
|
+
return Qo(i), i;
|
|
8894
8942
|
}
|
|
8895
|
-
async function
|
|
8896
|
-
let i = await
|
|
8943
|
+
async function ss(e, t, n, r) {
|
|
8944
|
+
let i = await Zo(), a = new Bo(), o = /* @__PURE__ */ new Set(), s = 0, c = null, l = () => {
|
|
8897
8945
|
s++, c?.();
|
|
8898
8946
|
};
|
|
8899
|
-
|
|
8900
|
-
let u =
|
|
8901
|
-
for (console.log(`๐ Worker scheduler started. Driver: ${i.name}. Concurrency: ${e}. Tags: ${r.tags.join(", ") || "(none)"}`);
|
|
8947
|
+
is = l, rs = new AbortController();
|
|
8948
|
+
let u = cs(r, rs.signal, l);
|
|
8949
|
+
for (console.log(`๐ Worker scheduler started. Driver: ${i.name}. Concurrency: ${e}. Tags: ${r.tags.join(", ") || "(none)"}`); !ts.isStopping;) {
|
|
8902
8950
|
let n = s;
|
|
8903
8951
|
try {
|
|
8904
|
-
for (; o.size < e &&
|
|
8952
|
+
for (; o.size < e && !ts.isStopping;) {
|
|
8905
8953
|
let e = await t.claimNextJob(r.tags);
|
|
8906
8954
|
if (!e) break;
|
|
8907
|
-
if (
|
|
8955
|
+
if (ts.isStopping) {
|
|
8908
8956
|
await t.releaseJob(e.id);
|
|
8909
8957
|
break;
|
|
8910
8958
|
}
|
|
8911
8959
|
let n = process.env.WORKER_NAME || "cli", s;
|
|
8912
8960
|
s = (async () => {
|
|
8913
|
-
|
|
8961
|
+
ds(r, e.id);
|
|
8914
8962
|
let o = await a.getRevision(e.workflow_id, e.workflow_revision);
|
|
8915
8963
|
if (!o) throw await t.finishJob(e.id, "failed"), Error(`Workflow ${e.workflow_id} revision ${e.workflow_revision} was not found`);
|
|
8916
8964
|
let s = new ce();
|
|
8917
|
-
s.replace(await
|
|
8965
|
+
s.replace(await fs(r, n, e.id)), await ps({
|
|
8918
8966
|
workerId: n,
|
|
8919
8967
|
job: e,
|
|
8920
8968
|
queue: t,
|
|
@@ -8924,17 +8972,17 @@ async function as(e, t, n, r) {
|
|
|
8924
8972
|
workflow: o
|
|
8925
8973
|
});
|
|
8926
8974
|
})().catch((e) => console.error(`[${n}] โ ๏ธ Worker execution error:`, e)).finally(() => {
|
|
8927
|
-
o.delete(s),
|
|
8975
|
+
o.delete(s), ds(r, e.id), l();
|
|
8928
8976
|
}), o.add(s);
|
|
8929
8977
|
}
|
|
8930
8978
|
} catch (e) {
|
|
8931
8979
|
console.error("โ ๏ธ Worker scheduler claim error:", e), setTimeout(l, 5e3).unref();
|
|
8932
8980
|
}
|
|
8933
|
-
|
|
8981
|
+
!ts.isStopping && s === n && (await ls(n, () => s, (e) => c = e), c = null);
|
|
8934
8982
|
}
|
|
8935
|
-
|
|
8983
|
+
rs.abort(), await Promise.allSettled(o), await u, is = null, console.log("๐ Worker scheduler stopped cleanly.");
|
|
8936
8984
|
}
|
|
8937
|
-
async function
|
|
8985
|
+
async function cs(e, t, n) {
|
|
8938
8986
|
let r = 1e3;
|
|
8939
8987
|
for (; !t.aborted;) {
|
|
8940
8988
|
try {
|
|
@@ -8945,10 +8993,10 @@ async function os(e, t, n) {
|
|
|
8945
8993
|
if (t.aborted || e?.name === "AbortError") break;
|
|
8946
8994
|
console.error(`โ ๏ธ Worker event stream disconnected: ${e.message}`);
|
|
8947
8995
|
}
|
|
8948
|
-
await
|
|
8996
|
+
await us(r, t), r = Math.min(r * 2, 3e4);
|
|
8949
8997
|
}
|
|
8950
8998
|
}
|
|
8951
|
-
function
|
|
8999
|
+
function ls(e, t, n) {
|
|
8952
9000
|
return new Promise((r) => {
|
|
8953
9001
|
if (t() !== e) return r();
|
|
8954
9002
|
let i = setTimeout(r, 6e4);
|
|
@@ -8957,7 +9005,7 @@ function ss(e, t, n) {
|
|
|
8957
9005
|
});
|
|
8958
9006
|
});
|
|
8959
9007
|
}
|
|
8960
|
-
function
|
|
9008
|
+
function us(e, t) {
|
|
8961
9009
|
return new Promise((n) => {
|
|
8962
9010
|
if (t.aborted) return n();
|
|
8963
9011
|
let r = () => {
|
|
@@ -8968,7 +9016,7 @@ function cs(e, t) {
|
|
|
8968
9016
|
t.addEventListener("abort", r, { once: !0 });
|
|
8969
9017
|
});
|
|
8970
9018
|
}
|
|
8971
|
-
async function
|
|
9019
|
+
async function ds(e, t) {
|
|
8972
9020
|
if (e.workerToken) try {
|
|
8973
9021
|
let n = await fetch(new URL("/api/events", e.serverUrl), {
|
|
8974
9022
|
method: "POST",
|
|
@@ -8979,12 +9027,12 @@ async function ls(e, t) {
|
|
|
8979
9027
|
body: JSON.stringify({ jobId: t }),
|
|
8980
9028
|
signal: AbortSignal.timeout(5e3)
|
|
8981
9029
|
});
|
|
8982
|
-
!n.ok &&
|
|
9030
|
+
!n.ok && es && console.error(`Failed to publish job status event: HTTP ${n.status}`);
|
|
8983
9031
|
} catch (e) {
|
|
8984
|
-
|
|
9032
|
+
es && console.error("Failed to publish job status event:", e);
|
|
8985
9033
|
}
|
|
8986
9034
|
}
|
|
8987
|
-
async function
|
|
9035
|
+
async function fs(e, t, n) {
|
|
8988
9036
|
if (!e.workerToken) return {};
|
|
8989
9037
|
try {
|
|
8990
9038
|
let r = await fetch(new URL(`/api/jobs/${n}/secrets`, e.serverUrl), {
|
|
@@ -9000,7 +9048,7 @@ async function us(e, t, n) {
|
|
|
9000
9048
|
throw console.error(`Unable to retrieve secrets for job ${n}:`, e), e;
|
|
9001
9049
|
}
|
|
9002
9050
|
}
|
|
9003
|
-
async function
|
|
9051
|
+
async function ps(e) {
|
|
9004
9052
|
let { workerId: t, job: n, config: r, secrets: a, queue: o } = e;
|
|
9005
9053
|
console.log(`\n[${t}] ๐ฆ Claimed Job #${n.id} (Workflow: ${n.workflow_id})`);
|
|
9006
9054
|
let s = typeof n.payload == "string" ? JSON.parse(n.payload) : n.payload;
|
|
@@ -9020,27 +9068,27 @@ async function ds(e) {
|
|
|
9020
9068
|
durationMs: 0,
|
|
9021
9069
|
outputs: {},
|
|
9022
9070
|
logContent: ""
|
|
9023
|
-
})), g =
|
|
9024
|
-
await o.saveReport(n.id, g),
|
|
9025
|
-
let _ = new
|
|
9071
|
+
})), g = bs(n, "running", u, l, m.env, h, s);
|
|
9072
|
+
await o.saveReport(n.id, g), ds(r, n.id);
|
|
9073
|
+
let _ = new $o(r.plugins), v = {
|
|
9026
9074
|
jobId: String(n.id),
|
|
9027
9075
|
workflowName: n.workflow_id,
|
|
9028
9076
|
inputs: l,
|
|
9029
9077
|
runUrl: new URL(`/runs/${n.id}`, r.serverUrl).toString()
|
|
9030
9078
|
};
|
|
9031
9079
|
await _.triggerWorkflowStart(v);
|
|
9032
|
-
let { cancelled: y, failed: ee } = await
|
|
9080
|
+
let { cancelled: y, failed: ee } = await ms({
|
|
9033
9081
|
payload: s,
|
|
9034
9082
|
steps: c,
|
|
9035
9083
|
executionContext: m,
|
|
9036
9084
|
...e
|
|
9037
9085
|
}, g), b = y ? "cancelled" : ee ? "failed" : "success";
|
|
9038
|
-
g.status = b, g.durationMs = Date.now() - u, g.finishedAt = (/* @__PURE__ */ new Date()).toISOString(), await o.completeJob(n.id, b, g),
|
|
9086
|
+
g.status = b, g.durationMs = Date.now() - u, g.finishedAt = (/* @__PURE__ */ new Date()).toISOString(), await o.completeJob(n.id, b, g), ds(r, n.id), console.log(`[${t}] โ
Job #${n.id} completed as: ${b}`), await _.triggerWorkflowFinish(v, b);
|
|
9039
9087
|
}
|
|
9040
|
-
async function
|
|
9088
|
+
async function ms(e, t) {
|
|
9041
9089
|
let { driver: n, workerId: r, queue: i, config: a, job: o, payload: s, steps: c, executionContext: l } = e, u = t.steps, d = !1, f = !1, p = 0;
|
|
9042
9090
|
try {
|
|
9043
|
-
e.workflow?.env && Object.assign(l.env, await
|
|
9091
|
+
e.workflow?.env && Object.assign(l.env, await _s(e.workflow.env, l));
|
|
9044
9092
|
for (let e = 0; e < c.length; e++) {
|
|
9045
9093
|
let s = c[e], m = (/* @__PURE__ */ new Date()).toISOString();
|
|
9046
9094
|
u[e] = {
|
|
@@ -9053,8 +9101,8 @@ async function fs(e, t) {
|
|
|
9053
9101
|
logContent: "",
|
|
9054
9102
|
startedAt: m,
|
|
9055
9103
|
finishedAt: void 0
|
|
9056
|
-
}, t.durationMs = Date.now() - Date.parse(t.startedAt), await i.saveReport(o.id, t),
|
|
9057
|
-
let h = await
|
|
9104
|
+
}, t.durationMs = Date.now() - Date.parse(t.startedAt), await i.saveReport(o.id, t), ds(a, o.id);
|
|
9105
|
+
let h = await hs({
|
|
9058
9106
|
workerId: r,
|
|
9059
9107
|
jobId: o.id,
|
|
9060
9108
|
step: s,
|
|
@@ -9081,7 +9129,7 @@ async function fs(e, t) {
|
|
|
9081
9129
|
exitCode: 1,
|
|
9082
9130
|
error: "Step failed before execution started",
|
|
9083
9131
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9084
|
-
}, p = e + 1, t.durationMs = Date.now() - Date.parse(t.startedAt), await i.saveReport(o.id, t),
|
|
9132
|
+
}, p = e + 1, t.durationMs = Date.now() - Date.parse(t.startedAt), await i.saveReport(o.id, t), ds(a, o.id), h.skipped) break;
|
|
9085
9133
|
if (h.cancelled) {
|
|
9086
9134
|
f = !0, d = !0;
|
|
9087
9135
|
break;
|
|
@@ -9094,22 +9142,22 @@ async function fs(e, t) {
|
|
|
9094
9142
|
} catch (e) {
|
|
9095
9143
|
console.log("๐ Step failed", e), d = !0;
|
|
9096
9144
|
}
|
|
9097
|
-
return p < c.length && (
|
|
9145
|
+
return p < c.length && (ys(c, p, u), t.durationMs = Date.now() - Date.parse(t.startedAt), await i.saveReport(o.id, t), ds(a, o.id)), {
|
|
9098
9146
|
failed: d,
|
|
9099
9147
|
cancelled: f,
|
|
9100
9148
|
stepReports: u
|
|
9101
9149
|
};
|
|
9102
9150
|
}
|
|
9103
|
-
async function
|
|
9151
|
+
async function hs(e) {
|
|
9104
9152
|
let { step: t, stepIndex: n, executionContext: r } = e;
|
|
9105
|
-
if (t.id ||= `step-${n}`, t.name ||= t.id, t.if && !await Cn.evaluateConditions(t.if, r)) return
|
|
9153
|
+
if (t.id ||= `step-${n}`, t.name ||= t.id, t.if && !await Cn.evaluateConditions(t.if, r)) return es && console.log(`โฉ Skipped step ${t.id} based on condition: ${t.if}`, r), {
|
|
9106
9154
|
failed: !1,
|
|
9107
9155
|
cancelled: !1,
|
|
9108
9156
|
skipped: !0,
|
|
9109
9157
|
report: null
|
|
9110
9158
|
};
|
|
9111
9159
|
try {
|
|
9112
|
-
let n = await
|
|
9160
|
+
let n = await _s(t.env, r), i = {
|
|
9113
9161
|
jobId: String(e.jobId),
|
|
9114
9162
|
step: t,
|
|
9115
9163
|
command: t.run,
|
|
@@ -9123,15 +9171,15 @@ async function ps(e) {
|
|
|
9123
9171
|
WORKING_DIR: r.workingDir
|
|
9124
9172
|
}
|
|
9125
9173
|
};
|
|
9126
|
-
return t.eval ?
|
|
9174
|
+
return t.eval ? gs({
|
|
9127
9175
|
...e,
|
|
9128
9176
|
stepContext: i
|
|
9129
|
-
}) :
|
|
9177
|
+
}) : vs({
|
|
9130
9178
|
...e,
|
|
9131
9179
|
stepContext: i
|
|
9132
9180
|
});
|
|
9133
9181
|
} catch (e) {
|
|
9134
|
-
return
|
|
9182
|
+
return es && console.error(`โฉ Failed to run step ${t.id}`, r, e), {
|
|
9135
9183
|
failed: !0,
|
|
9136
9184
|
cancelled: !1,
|
|
9137
9185
|
skipped: !1,
|
|
@@ -9139,7 +9187,7 @@ async function ps(e) {
|
|
|
9139
9187
|
};
|
|
9140
9188
|
}
|
|
9141
9189
|
}
|
|
9142
|
-
async function
|
|
9190
|
+
async function gs(e) {
|
|
9143
9191
|
let { queue: t, stepContext: n, executionContext: r } = e, { jobId: i, step: a } = n, o = Date.now(), s = a.id, c = a.name;
|
|
9144
9192
|
try {
|
|
9145
9193
|
let e = await Cn.evaluateExpression(a.eval, r);
|
|
@@ -9185,12 +9233,12 @@ async function ms(e) {
|
|
|
9185
9233
|
};
|
|
9186
9234
|
}
|
|
9187
9235
|
}
|
|
9188
|
-
async function
|
|
9236
|
+
async function _s(e, t) {
|
|
9189
9237
|
let n = {};
|
|
9190
9238
|
if (e) for (let [r, i] of Object.entries(e)) n[r] = String(await Cn.evaluateValue(i, t));
|
|
9191
9239
|
return n;
|
|
9192
9240
|
}
|
|
9193
|
-
async function
|
|
9241
|
+
async function vs(t) {
|
|
9194
9242
|
let { workerId: n, jobId: r, stepContext: i, executionContext: a, driver: o, queue: s } = t, { step: c } = i, l = c.id, u = c.name, d;
|
|
9195
9243
|
try {
|
|
9196
9244
|
d = o.execute(i);
|
|
@@ -9205,11 +9253,11 @@ async function gs(t) {
|
|
|
9205
9253
|
logFilePath: ""
|
|
9206
9254
|
};
|
|
9207
9255
|
}
|
|
9208
|
-
|
|
9256
|
+
ns.add(d);
|
|
9209
9257
|
let f = !1, p = setInterval(async () => {
|
|
9210
9258
|
await s.isCancelled(r) && (console.log(`[${n}] ๐ Job #${r} was cancelled! Halting execution.`), f = !0, clearInterval(p), await d.cancel());
|
|
9211
9259
|
}, 3e3), m = await d.done;
|
|
9212
|
-
if (clearInterval(p),
|
|
9260
|
+
if (clearInterval(p), ns.delete(d), d.logFilePath && e.existsSync(d.logFilePath)) try {
|
|
9213
9261
|
let e = await o.readLog(d.logFilePath);
|
|
9214
9262
|
await s.saveStepLog(r, l, e);
|
|
9215
9263
|
} catch (e) {
|
|
@@ -9236,7 +9284,7 @@ async function gs(t) {
|
|
|
9236
9284
|
}
|
|
9237
9285
|
};
|
|
9238
9286
|
}
|
|
9239
|
-
function
|
|
9287
|
+
function ys(e, t, n) {
|
|
9240
9288
|
for (let r = t; r < e.length; r++) {
|
|
9241
9289
|
let t = e[r], i = t.id || `step-${r}`;
|
|
9242
9290
|
n[r] = {
|
|
@@ -9250,7 +9298,7 @@ function _s(e, t, n) {
|
|
|
9250
9298
|
};
|
|
9251
9299
|
}
|
|
9252
9300
|
}
|
|
9253
|
-
function
|
|
9301
|
+
function bs(e, t, n, r, i, a, o) {
|
|
9254
9302
|
return {
|
|
9255
9303
|
jobId: String(e.id),
|
|
9256
9304
|
parentId: String(e.parentId || ""),
|
|
@@ -9271,7 +9319,7 @@ function vs(e, t, n, r, i, a, o) {
|
|
|
9271
9319
|
}
|
|
9272
9320
|
//#endregion
|
|
9273
9321
|
//#region src/scheduler.ts
|
|
9274
|
-
function
|
|
9322
|
+
function xs(e, t, n, r) {
|
|
9275
9323
|
return e.split(",").some((e) => {
|
|
9276
9324
|
let [i, a] = e.split("/"), o = a ? Number(a) : 1;
|
|
9277
9325
|
if (!Number.isInteger(o) || o < 1) return !1;
|
|
@@ -9279,7 +9327,7 @@ function ys(e, t, n, r) {
|
|
|
9279
9327
|
return Number.isInteger(l) && Number.isInteger(u) && t >= l && t <= u && (t - l) % o === 0;
|
|
9280
9328
|
});
|
|
9281
9329
|
}
|
|
9282
|
-
function
|
|
9330
|
+
function Ss(e, t, n = "UTC") {
|
|
9283
9331
|
let r = e.trim().split(/\s+/);
|
|
9284
9332
|
if (r.length !== 5) return !1;
|
|
9285
9333
|
let i = new Intl.DateTimeFormat("en-US", {
|
|
@@ -9301,16 +9349,16 @@ function bs(e, t, n = "UTC") {
|
|
|
9301
9349
|
"Thu",
|
|
9302
9350
|
"Fri",
|
|
9303
9351
|
"Sat"
|
|
9304
|
-
].indexOf(i.weekday), o =
|
|
9305
|
-
return
|
|
9352
|
+
].indexOf(i.weekday), o = xs(r[2], Number(i.day), 1, 31), s = xs(r[4].replace(/7/g, "0"), a, 0, 6);
|
|
9353
|
+
return xs(r[0], Number(i.minute), 0, 59) && xs(r[1], Number(i.hour), 0, 23) && xs(r[3], Number(i.month), 1, 12) && (r[2] === "*" || r[4] === "*" ? o && s : o || s);
|
|
9306
9354
|
}
|
|
9307
|
-
function
|
|
9355
|
+
function Cs(e) {
|
|
9308
9356
|
if (!e) return 0;
|
|
9309
9357
|
let t = e.match(/^([+-]?)(\d+)(m|h)$/);
|
|
9310
9358
|
if (!t) throw Error(`Invalid solar offset: ${e}`);
|
|
9311
9359
|
return (t[1] === "-" ? -1 : 1) * Number(t[2]) * (t[3] === "h" ? 36e5 : 6e4);
|
|
9312
9360
|
}
|
|
9313
|
-
function
|
|
9361
|
+
function ws(e, t) {
|
|
9314
9362
|
let n = t.latitude, r = t.longitude, i = Date.UTC(e.getUTCFullYear(), e.getUTCMonth(), e.getUTCDate()), a = Math.floor((i - Date.UTC(e.getUTCFullYear(), 0, 0)) / 864e5), o = r / 15, s = a + ((t.event === "sunrise" ? 6 : 18) - o) / 24, c = .9856 * s - 3.289, l = c + 1.916 * Math.sin(c * Math.PI / 180) + .02 * Math.sin(2 * c * Math.PI / 180) + 282.634;
|
|
9315
9363
|
l = (l + 360) % 360;
|
|
9316
9364
|
let u = Math.atan(.91764 * Math.tan(l * Math.PI / 180)) * 180 / Math.PI;
|
|
@@ -9318,9 +9366,9 @@ function Ss(e, t) {
|
|
|
9318
9366
|
let d = .39782 * Math.sin(l * Math.PI / 180), f = Math.cos(Math.asin(d)), p = (Math.cos(90.833 * Math.PI / 180) - d * Math.sin(n * Math.PI / 180)) / (f * Math.cos(n * Math.PI / 180));
|
|
9319
9367
|
if (p > 1 || p < -1) return null;
|
|
9320
9368
|
let m = (t.event === "sunrise" ? 360 - Math.acos(p) * 180 / Math.PI : Math.acos(p) * 180 / Math.PI) / 15;
|
|
9321
|
-
return m = (m + u - .06571 * s - 6.622 - o + 24) % 24, new Date(i + m * 36e5 +
|
|
9369
|
+
return m = (m + u - .06571 * s - 6.622 - o + 24) % 24, new Date(i + m * 36e5 + Cs(t.offset));
|
|
9322
9370
|
}
|
|
9323
|
-
var
|
|
9371
|
+
var Ts = class {
|
|
9324
9372
|
queue;
|
|
9325
9373
|
workflows;
|
|
9326
9374
|
config;
|
|
@@ -9337,9 +9385,9 @@ var Cs = class {
|
|
|
9337
9385
|
async tick(e = /* @__PURE__ */ new Date()) {
|
|
9338
9386
|
let t = /* @__PURE__ */ new Date(Math.floor(e.getTime() / 6e4) * 6e4);
|
|
9339
9387
|
for (let { definition: e, revision: n } of await this.workflows.published()) {
|
|
9340
|
-
for (let [r, i] of (e.schedule || []).entries())
|
|
9388
|
+
for (let [r, i] of (e.schedule || []).entries()) Ss(i.cron, t, i.timezone || "UTC") && await this.enqueue(e, n, i, `cron-${r}`, t);
|
|
9341
9389
|
for (let [r, i] of (e.solar || []).entries()) {
|
|
9342
|
-
let a =
|
|
9390
|
+
let a = ws(t, i);
|
|
9343
9391
|
a && Math.floor(a.getTime() / 6e4) === t.getTime() / 6e4 && await this.enqueue(e, n, i, `solar-${r}`, t);
|
|
9344
9392
|
}
|
|
9345
9393
|
}
|
|
@@ -9355,7 +9403,7 @@ var Cs = class {
|
|
|
9355
9403
|
} } };
|
|
9356
9404
|
await this.queue.enqueue(e.id, t, o, e.tags), console.log(`Scheduled ${e.id} via ${a}`);
|
|
9357
9405
|
}
|
|
9358
|
-
},
|
|
9406
|
+
}, Es = class {
|
|
9359
9407
|
name = "github-commit-status";
|
|
9360
9408
|
token;
|
|
9361
9409
|
apiUrl;
|
|
@@ -9397,13 +9445,13 @@ var Cs = class {
|
|
|
9397
9445
|
};
|
|
9398
9446
|
//#endregion
|
|
9399
9447
|
//#region src/index.ts
|
|
9400
|
-
async function
|
|
9448
|
+
async function Ds() {
|
|
9401
9449
|
let { config: e, command: t } = await oe();
|
|
9402
9450
|
e || process.exit(1);
|
|
9403
9451
|
let n = new ce(), r = new se(process.env.WORKER_NAME || "cli");
|
|
9404
9452
|
switch (t) {
|
|
9405
9453
|
case "start-server":
|
|
9406
|
-
console.log("๐ Starting Ingress Gateway..."), await r.init(), await
|
|
9454
|
+
console.log("๐ Starting Ingress Gateway..."), await r.init(), await qo.withPort({
|
|
9407
9455
|
config: e,
|
|
9408
9456
|
queue: r,
|
|
9409
9457
|
secrets: n,
|
|
@@ -9412,13 +9460,13 @@ async function Ts() {
|
|
|
9412
9460
|
});
|
|
9413
9461
|
break;
|
|
9414
9462
|
case "start-workers":
|
|
9415
|
-
console.log(`โ๏ธ Starting worker scheduler with ${e.workers} concurrent slot(s)...`), await r.init(),
|
|
9463
|
+
console.log(`โ๏ธ Starting worker scheduler with ${e.workers} concurrent slot(s)...`), await r.init(), os(e.workers, r, n, e);
|
|
9416
9464
|
break;
|
|
9417
9465
|
case "start-scheduler": {
|
|
9418
9466
|
console.log("Starting workflow scheduler..."), await r.init();
|
|
9419
9467
|
let t = new Bo();
|
|
9420
9468
|
await t.init();
|
|
9421
|
-
let n = new
|
|
9469
|
+
let n = new Ts(r, t, e);
|
|
9422
9470
|
n.start();
|
|
9423
9471
|
let i = () => {
|
|
9424
9472
|
n.stop(), process.exit(0);
|
|
@@ -9429,6 +9477,6 @@ async function Ts() {
|
|
|
9429
9477
|
default: console.error(`โ Unknown command: '${t}'`), ae(), process.exit(1);
|
|
9430
9478
|
}
|
|
9431
9479
|
}
|
|
9432
|
-
|
|
9480
|
+
Ds().catch(console.error);
|
|
9433
9481
|
//#endregion
|
|
9434
|
-
export {
|
|
9482
|
+
export { Es as GitHubStatusPlugin };
|
package/dist/queue.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export declare class QueueManager {
|
|
|
19
19
|
*/
|
|
20
20
|
finishJob(jobId: string | number, status: JobStatus): Promise<void>;
|
|
21
21
|
completeJob(jobId: string | number, status: JobStatus, report: WorkflowExecutionReport): Promise<void>;
|
|
22
|
+
cancelJob(jobId: string | number): Promise<'cancelled' | 'not_found' | 'not_active'>;
|
|
22
23
|
restartJob(jobId: string | number): Promise<any>;
|
|
23
24
|
/**
|
|
24
25
|
* Checks if the current job has been marked for cancellation by another event
|
package/dist/queue.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvF,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ;gBAAR,QAAQ,EAAE,MAAM;IAE9B,IAAI;IAKV;;;OAGG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,YAAY,GAAE,MAAM,EAAO,EAAE,cAAc,CAAC,EAAE,MAAM;IAoBrI;;;OAGG;IACG,YAAY,CAAC,UAAU,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IA6BlE,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOvD;;OAEG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,SAAS;IAInD,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvF,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ;gBAAR,QAAQ,EAAE,MAAM;IAE9B,IAAI;IAKV;;;OAGG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,YAAY,GAAE,MAAM,EAAO,EAAE,cAAc,CAAC,EAAE,MAAM;IAoBrI;;;OAGG;IACG,YAAY,CAAC,UAAU,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IA6BlE,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOvD;;OAEG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,SAAS;IAInD,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IAUtG,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,WAAW,GAAG,YAAY,CAAC;IAapF,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAiBvC;;OAEG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKrD,cAAc;IAMd,YAAY;IAsClB;;OAEG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAIlD;;OAEG;IACG,QAAQ,CAAC,KAAK,SAAK,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAiB/E;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IAOxF,2DAA2D;IACrD,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU5F;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAS1E"}
|
|
@@ -5,6 +5,7 @@ export declare class SecretRepository {
|
|
|
5
5
|
set(name: string, value: string): Promise<void>;
|
|
6
6
|
getAll(): Promise<Record<string, string>>;
|
|
7
7
|
names(): Promise<string[]>;
|
|
8
|
+
delete(name: string): Promise<boolean>;
|
|
8
9
|
private decrypt;
|
|
9
10
|
}
|
|
10
11
|
//# sourceMappingURL=secret-repository.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-repository.d.ts","sourceRoot":"","sources":["../src/secret-repository.ts"],"names":[],"mappings":"AAWA,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,GAAG,CAAC,CAAS;IAEf,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B,OAAO,CAAC,aAAa;IAIf,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU/C,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAKzC,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"secret-repository.d.ts","sourceRoot":"","sources":["../src/secret-repository.ts"],"names":[],"mappings":"AAWA,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,GAAG,CAAC,CAAS;IAEf,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B,OAAO,CAAC,aAAa;IAIf,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU/C,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAKzC,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAK1B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAK5C,OAAO,CAAC,OAAO;CAMhB"}
|
package/dist/server.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export declare class WebhookServer {
|
|
|
36
36
|
private handleWorkflowPublish;
|
|
37
37
|
private handleSecretList;
|
|
38
38
|
private handleSecretSave;
|
|
39
|
+
private handleSecretDelete;
|
|
39
40
|
private handleJobSecrets;
|
|
40
41
|
/**
|
|
41
42
|
* Handles Zero-Downtime Secret Reload
|
|
@@ -52,6 +53,7 @@ export declare class WebhookServer {
|
|
|
52
53
|
*/
|
|
53
54
|
private renderRunDetails;
|
|
54
55
|
private handleRestartJob;
|
|
56
|
+
private handleCancelJob;
|
|
55
57
|
private currentSecrets;
|
|
56
58
|
private redact;
|
|
57
59
|
private redactText;
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAEV,mBAAmB,EACnB,oBAAoB,EAErB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAEV,mBAAmB,EACnB,oBAAoB,EAErB,MAAM,YAAY,CAAC;AAUpB,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,aAAa,CAA0C;IAC/D,OAAO,CAAC,SAAS,CAA4B;IAC7C,OAAO,CAAC,gBAAgB,CAA0B;IAClD,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,eAAe,CAAgB;WAE1B,QAAQ,CAAC,OAAO,EAAE,oBAAoB,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;gBAK1D,OAAO,EAAE,oBAAoB;IAYzC,oBAAoB,CAAC,YAAY,EAAE,mBAAmB;YAIxC,aAAa;IAiH3B;;OAEG;YACW,aAAa;YA0Bb,WAAW;YAwBX,UAAU;YAuBV,cAAc;IAuC5B,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,YAAY;IAmBpB,OAAO,CAAC,YAAY;YAON,QAAQ;YAYR,wBAAwB;YAcxB,kBAAkB;YAOlB,iBAAiB;YAWjB,kBAAkB;YAiBlB,oBAAoB;YASpB,qBAAqB;YAWrB,gBAAgB;YAMhB,gBAAgB;YAgBhB,kBAAkB;YASlB,gBAAgB;IAc9B;;OAEG;YACW,kBAAkB;YAclB,iBAAiB;IAuB/B;;OAEG;YACW,eAAe;YAQf,mBAAmB;IAWjC;;OAEG;YACW,gBAAgB;YAuBhB,gBAAgB;YAehB,eAAe;YAkBf,cAAc;IAS5B,OAAO,CAAC,MAAM;YAMA,UAAU;IAIxB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAStC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAS5B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflows-ui.d.ts","sourceRoot":"","sources":["../src/workflows-ui.ts"],"names":[],"mappings":"AAEA,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,UAAU,SAAK,GAAG,MAAM,CAI/F"}
|