spb/spbgit Public MIT
SPB Git — the platform hosting itself
JavaScript 73.9%
CSS 11.7%
Nunjucks 11.6%
Shell 2.7%
1/**2 * ─────────────────────────────────────────────3 * SPB Git — Personal Git Platform4 * ─────────────────────────────────────────────5 * Author : Simon-Pierre Boucher6 * Contact : contact@spboucher.ai7 * File : cli/commands/release.mjs8 * Purpose : `spbgit release upload|list|rm` — binary assets on tags9 * License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213import { createReadStream, statSync, existsSync } from 'node:fs';14import { basename } from 'node:path';15import pc from 'picocolors';16import { requireCliConfig } from '../lib/config.mjs';17import { api, EXIT } from '../lib/api.mjs';18import { printTable } from '../lib/ui.mjs';1920function humanBytes(bytes) {21 const units = ['B', 'KB', 'MB', 'GB'];22 let value = Number(bytes) || 0;23 let i = 0;24 while (value >= 1024 && i < units.length - 1) {25 value /= 1024;26 i += 1;27 }28 return `${i === 0 ? value : value.toFixed(1)} ${units[i]}`;29}3031export function registerRelease(program) {32 const release = program.command('release').description('manage release assets (dmg, pkg, zip…) attached to tags');3334 release35 .command('upload <repo> <tag> <files...>')36 .description('upload one or more files as release assets on an existing tag')37 .action(async (repo, tag, files) => {38 const config = requireCliConfig();39 let failures = 0;40 for (const file of files) {41 if (!existsSync(file)) {42 console.error(`${pc.red('✗')} ${file}: file not found`);43 failures += 1;44 continue;45 }46 const name = basename(file);47 const size = statSync(file).size;48 process.stdout.write(`${pc.dim('↑')} ${name} (${humanBytes(size)})… `);49 let response;50 try {51 response = await fetch(52 `${config.server}/api/v1/repos/${encodeURIComponent(repo)}/releases/${encodeURIComponent(tag)}/assets/${encodeURIComponent(name)}`,53 {54 method: 'PUT',55 headers: {56 Authorization: `Bearer ${config.token}`,57 'Content-Type': 'application/octet-stream',58 'Content-Length': String(size),59 },60 body: createReadStream(file),61 duplex: 'half',62 },63 );64 } catch (err) {65 console.log(pc.red(`network error: ${err.cause?.code ?? err.message}`));66 failures += 1;67 continue;68 }69 const body = await response.json().catch(() => null);70 if (!response.ok) {71 console.log(pc.red(`failed: ${body?.error?.message ?? `HTTP ${response.status}`}`));72 failures += 1;73 continue;74 }75 console.log(pc.green('✓'));76 console.log(` ${pc.dim('url')} ${body.url}`);77 console.log(` ${pc.dim('sha256')} ${body.sha256}`);78 }79 if (failures > 0) process.exit(EXIT.PARTIAL);80 });8182 release83 .command('list <repo>')84 .description('list releases and their assets')85 .option('--json', 'machine-readable output')86 .action(async (repo, opts) => {87 const config = requireCliConfig();88 const { releases } = await api(config, 'GET', `/api/v1/repos/${encodeURIComponent(repo)}/releases`, undefined, { auth: false });89 if (opts.json) {90 console.log(JSON.stringify(releases, null, 2));91 return;92 }93 if (releases.length === 0) {94 console.log(pc.dim('No tags yet. Tag a version first: git tag v1.0.0 && git push --tags'));95 return;96 }97 const rows = [[pc.bold('TAG'), pc.bold('ASSET'), pc.bold('SIZE'), pc.bold('UPLOADED')]];98 for (const r of releases) {99 if (r.assets.length === 0) {100 rows.push([pc.bold(r.tag), pc.dim('(source archives only)'), '', '']);101 continue;102 }103 r.assets.forEach((a, i) => {104 rows.push([i === 0 ? pc.bold(r.tag) : '', a.name, humanBytes(a.size), a.uploaded?.slice(0, 10) ?? '']);105 });106 }107 printTable(rows);108 });109110 release111 .command('rm <repo> <tag> <asset>')112 .description('delete a release asset')113 .action(async (repo, tag, asset) => {114 const config = requireCliConfig();115 await api(config, 'DELETE', `/api/v1/repos/${encodeURIComponent(repo)}/releases/${encodeURIComponent(tag)}/assets/${encodeURIComponent(asset)}`);116 console.log(`${pc.green('✓')} ${asset} removed from ${repo}@${tag}`);117 });118}119