// // Job logs. Nova has no output panel an extension can write to, so logs open in // the editor — saved under .tmp/gitea-logs/ when the preference allows it. // const config = require("../util/config.js"); const nodes = require("../views/nodes.js"); const selection = require("./selection.js"); const ui = require("../ui.js"); const FAILED = new Set(["failure", "failed", "error", "timed_out"]); function logPath(repo, run, job) { if (!config.saveLogsToRepo() || !repo.dir) return null; const safe = String(job.name || job.id).replace(/[^A-Za-z0-9._-]+/g, "-"); return nova.path.join( repo.dir, ".tmp", "gitea-logs", `${run ? run.id : "run"}-${job.id}-${safe}.log`, ); } function header(repo, run, job) { return [ `# ${repo.fullName} — ${job.name || `job ${job.id}`}`, run ? `# Run: ${nodes.runTitle(run)} (#${run.run_number || run.id})` : null, `# Status: ${nodes.runState(job)}`, job.html_url ? `# ${job.html_url}` : null, "", "", ] .filter((line) => line !== null) .join("\n"); } async function openJobLogs(store, repo, run, job) { const api = store.apiFor(repo); if (!api) return; let text; try { text = await api.jobLogs(repo, job.id); } catch (error) { ui.error(`Could not fetch logs for ${job.name || job.id}: ${error.message || error}`); return; } if (!text || !text.trim()) { ui.info(`No logs are available yet for ${job.name || job.id}.`); return; } await ui.openText(header(repo, run, job) + text, { path: logPath(repo, run, job) }); } function register(store) { nova.commands.register("gitea.viewJobLogs", async (argument) => { const node = selection.selectedNode(argument); if (!node || !node.data || !node.data.job) { ui.warn("Select a job in the Gitea sidebar first."); return; } const repo = selection.repoOf(node); if (!repo) return; await openJobLogs(store, repo, node.data.run, node.data.job); }); nova.commands.register("gitea.openLatestFailedJobLogs", async (argument) => { // Invoked from a failure notification with an explicit run, or from the // menu, where the most recent failed run across all repositories wins. let repo = null; let run = null; if (argument && argument.repoKey) { repo = store.repoFor(argument.repoKey); const entry = repo ? store.runsFor(repo.key) : null; run = entry ? entry.runs.find((item) => String(item.id) === String(argument.runId)) : null; } if (!run) { const node = selection.selectedNode(argument); const nodeRun = selection.runFor(node); if (nodeRun && FAILED.has(nodes.runState(nodeRun))) { repo = selection.repoOf(node); run = nodeRun; } } if (!run) { let newest = null; for (const candidate of store.repos) { for (const item of store.runsFor(candidate.key).runs) { if (!FAILED.has(nodes.runState(item))) continue; const at = new Date(item.started_at || item.created_at || 0).getTime(); if (!newest || at > newest.at) newest = { at: at, repo: candidate, run: item }; } } if (!newest) { ui.info("No failed workflow runs were found."); return; } repo = newest.repo; run = newest.run; } const detail = await store.loadRunDetail(repo, run); const failed = (detail ? detail.jobs : []).filter((job) => FAILED.has(nodes.runState(job)), ); if (!failed.length) { ui.info(`No failed jobs in ${nodes.runTitle(run)}.`); return; } const job = failed.length === 1 ? failed[0] : await ui.choose(failed, { placeholder: "Open logs for which failed job?", label: (item) => item.name || `Job ${item.id}`, }); if (!job) return; await openJobLogs(store, repo, run, job); }); } exports.register = register; exports.openJobLogs = openJobLogs;