| 1 | /** |
| 2 | * Every job's log of a run's attempt as one zip |
| 3 | * (`actions/runs/:id/logs.zip?attempt=`), laid out as the API's archive is: |
| 4 | * `{n}_{job}.txt` whole, and `{job}/{step}_{step name}.txt`. For anyone who |
| 5 | * can see the run. |
| 6 | */ |
| 7 | import type { Route } from "./+types/actions-logs-download"; |
| 8 | import { archiveFiles } from "../../lib/log-lines"; |
| 9 | import { attachment, refused } from "../../lib/log-download.server"; |
| 10 | import { actions } from "../../lib/services.server"; |
| 11 | import { getViewer } from "../../lib/session.server"; |
| 12 | import { zip } from "../../lib/zip"; |
| 13 | |
| 14 | export async function loader({ params, context, request }: Route.LoaderArgs) { |
| 15 | const repo = { namespace: params.owner, name: params.repo }; |
| 16 | const asked = Number(new URL(request.url).searchParams.get("attempt") ?? ""); |
| 17 | const attempt = Number.isInteger(asked) && asked > 0 ? asked : undefined; |
| 18 | const logs = await actions.runLogs(repo, getViewer(context), params.id, attempt); |
| 19 | if (!logs.ok) return refused(logs.error.code === "not_found" ? 404 : 400, logs.error.message); |
| 20 | const encoder = new TextEncoder(); |
| 21 | const archive = await zip(archiveFiles(logs.value).map((file) => ({ path: file.path, data: encoder.encode(file.text) }))); |
| 22 | return attachment(archive, "application/zip", attempt ? `logs_${params.id}_attempt_${attempt}.zip` : `logs_${params.id}.zip`); |
| 23 | } |