artifacts.js (2619B)
1 // src/worker/artifacts.js - collecting and uploading task output 2 // 3 // Patterns are matched with the glob support built into node, so there is 4 // nothing to install. Matches are confined to the workspace: a task can write 5 // a symlink pointing anywhere it likes, and following one would upload a 6 // file the task was never given. 7 8 import fs from 'node:fs/promises'; 9 import path from 'node:path'; 10 import { Readable } from 'node:stream'; 11 import { createReadStream } from 'node:fs'; 12 13 const MAX_FILES = 2000; 14 15 export async function collectArtifacts(workspace, patterns, { logger = console } = {}) { 16 const root = await fs.realpath(workspace); 17 const seen = new Map(); 18 19 for (const pattern of patterns) { 20 let matches; 21 try { 22 matches = await Array.fromAsync(fs.glob(pattern, { cwd: root })); 23 } catch (e) { 24 logger.warn?.(`artifact pattern ${pattern} failed: ${e.message}`); 25 continue; 26 } 27 28 for (const relative of matches) { 29 if (seen.size >= MAX_FILES) { 30 logger.warn?.(`artifact limit of ${MAX_FILES} files reached, ignoring the rest`); 31 return [...seen.values()]; 32 } 33 34 const normalized = relative.split(path.sep).join('/'); 35 if (seen.has(normalized)) continue; 36 37 const absolute = path.join(root, relative); 38 let resolved; 39 let stat; 40 try { 41 resolved = await fs.realpath(absolute); 42 stat = await fs.stat(resolved); 43 } catch { 44 // Vanished between globbing and stat, or a broken symlink. 45 continue; 46 } 47 48 if (!stat.isFile()) continue; 49 if (resolved !== root && !resolved.startsWith(root + path.sep)) { 50 logger.warn?.(`refusing artifact ${normalized}: resolves outside the workspace`); 51 continue; 52 } 53 54 seen.set(normalized, { path: normalized, absolute: resolved, size: stat.size }); 55 } 56 } 57 58 return [...seen.values()]; 59 } 60 61 export function shouldCollect(when, success) { 62 if (when === 'always') return true; 63 if (when === 'on_failure') return !success; 64 return success; 65 } 66 67 export async function uploadArtifacts(client, url, files, { logger = console } = {}) { 68 const uploaded = []; 69 for (const file of files) { 70 try { 71 const body = Readable.toWeb(createReadStream(file.absolute)); 72 const result = await client.uploadArtifact(url, file.path, body, file.size); 73 uploaded.push(result); 74 } catch (e) { 75 // An artifact that cannot be stored is worth reporting, but the task 76 // itself already succeeded or failed on its own merits. 77 logger.warn?.(`could not upload ${file.path}: ${e.message}`); 78 } 79 } 80 return uploaded; 81 }