import { logUtil } from "../../utils/logger"; /** * Publish a .gem file to RubyGems.org. * * Uses the multipart form upload API: * POST https://rubygems.org/api/v1/gems * Authorization: * Content-Type: multipart/form-data * Body field "gem" = raw .gem bytes */ export async function publishGem( gemData: Buffer, filename: string, token: string, dryRun = false, ): Promise { const boundary = `--RubyGems${Date.now()}${Math.random().toString(36).slice(2)}`; if (dryRun) { logUtil.log("[rubygems] DRY RUN — skipping upload"); logUtil.log( `[rubygems] would publish ${filename} (${gemData.length} bytes)`, ); logUtil.log("[rubygems] POST https://rubygems.org/api/v1/gems"); logUtil.log("[rubygems] Authorization: "); logUtil.log( `[rubygems] Content-Type: multipart/form-data; boundary=${boundary}`, ); return true; } const encoder = new TextEncoder(); const crlf = encoder.encode("\r\n"); const parts: Uint8Array[] = []; // File field: gem= parts.push(encoder.encode(`--${boundary}\r\n`)); parts.push( encoder.encode( `Content-Disposition: form-data; name="gem"; filename="${filename}"\r\n`, ), ); parts.push(encoder.encode("Content-Type: application/octet-stream\r\n\r\n")); parts.push(new Uint8Array(gemData)); parts.push(crlf); // End boundary parts.push(encoder.encode(`--${boundary}--\r\n`)); const body = new Uint8Array(parts.reduce((acc, p) => acc + p.length, 0)); let offset = 0; for (const p of parts) { body.set(p, offset); offset += p.length; } try { const res = await fetch("https://rubygems.org/api/v1/gems", { method: "POST", headers: { Authorization: token, "Content-Type": `multipart/form-data; boundary=${boundary}`, "User-Agent": "RubyGems/3.5.0", }, body, }); if (res.ok) { logUtil.log(`[rubygems] Published ${filename}`); return true; } const text = await res.text(); logUtil.log( `[rubygems] Publish failed (${res.status}): ${text.slice(0, 300)}`, ); return false; } catch (e) { logUtil.log( `[rubygems] Publish error: ${e instanceof Error ? e.message : String(e)}`, ); return false; } }