SPB Git

spb/spbgit Public MIT

SPB Git — the platform hosting itself

JavaScript 73.9% CSS 11.7% Nunjucks 11.6% Shell 2.7%
4.6 KB · 119 lines javascript
Raw Blame History
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