#!/usr/bin/env node // Usage: GITEA_TOKEN=xxx node tools/release.js // Crée une release Gitea et upload les binaires dist/*.AppImage et dist/*.exe const https = require('https'); const fs = require('fs'); const path = require('path'); const TOKEN = process.env.GITEA_TOKEN; if (!TOKEN) { console.error('Erreur : variable GITEA_TOKEN manquante.\nUsage : GITEA_TOKEN=xxx node tools/release.js'); process.exit(1); } const OWNER = 'shamiiow'; const REPO = 'farm-tracker-sao'; const HOST = 'git.shamiiow.com'; const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')); const VERSION = pkg.version; const TAG = `v${VERSION}`; const DIST = path.join(__dirname, '..', 'dist'); const ASSETS = fs.readdirSync(DIST).filter(f => f.endsWith('.AppImage') || (f.endsWith('.exe') && !f.endsWith('.blockmap'))); if (!ASSETS.length) { console.error('Aucun binaire trouvé dans dist/. Lance "npm run dist" et "npm run dist:win" d\'abord.'); process.exit(1); } function apiRequest(method, apiPath, body, extraHeaders = {}) { return new Promise((resolve, reject) => { const bodyBuf = body ? Buffer.from(JSON.stringify(body)) : null; const options = { hostname: HOST, path: `/api/v1${apiPath}`, method, headers: { 'Authorization': `token ${TOKEN}`, 'Content-Type': 'application/json', 'Accept': 'application/json', ...extraHeaders, ...(bodyBuf ? { 'Content-Length': bodyBuf.length } : {}) } }; const req = https.request(options, (res) => { let data = ''; res.on('data', chunk => { data += chunk; }); res.on('end', () => { try { const parsed = JSON.parse(data); if (res.statusCode >= 400) reject(new Error(`HTTP ${res.statusCode}: ${parsed.message || data}`)); else resolve(parsed); } catch { reject(new Error(`Réponse non-JSON (${res.statusCode}): ${data.slice(0, 200)}`)); } }); }); req.on('error', reject); if (bodyBuf) req.write(bodyBuf); req.end(); }); } function uploadAsset(releaseId, filePath) { return new Promise((resolve, reject) => { const fileName = path.basename(filePath); const fileData = fs.readFileSync(filePath); const boundary = '----FormBoundary' + Math.random().toString(36).slice(2); const header = Buffer.from( `--${boundary}\r\nContent-Disposition: form-data; name="attachment"; filename="${fileName}"\r\nContent-Type: application/octet-stream\r\n\r\n` ); const footer = Buffer.from(`\r\n--${boundary}--\r\n`); const body = Buffer.concat([header, fileData, footer]); const options = { hostname: HOST, path: `/api/v1/repos/${OWNER}/${REPO}/releases/${releaseId}/assets`, method: 'POST', headers: { 'Authorization': `token ${TOKEN}`, 'Content-Type': `multipart/form-data; boundary=${boundary}`, 'Content-Length': body.length } }; const req = https.request(options, (res) => { let data = ''; res.on('data', chunk => { data += chunk; }); res.on('end', () => { if (res.statusCode >= 400) reject(new Error(`Upload HTTP ${res.statusCode}: ${data.slice(0, 200)}`)); else { try { resolve(JSON.parse(data)); } catch { resolve({}); } } }); }); req.on('error', reject); req.write(body); req.end(); }); } async function main() { console.log(`\n📦 Release ${TAG} — ${ASSETS.length} fichier(s) à uploader\n`); // Vérifie si la release existe déjà let release; try { release = await apiRequest('GET', `/repos/${OWNER}/${REPO}/releases/tags/${TAG}`); console.log(`Release ${TAG} existante (id ${release.id}), ajout des assets...`); } catch (_) { console.log(`Création de la release ${TAG}...`); release = await apiRequest('POST', `/repos/${OWNER}/${REPO}/releases`, { tag_name: TAG, name: `Farm Tracker ${TAG}`, body: `Release ${TAG}`, draft: false, prerelease: false }); console.log(` Release créée (id ${release.id})`); } for (const file of ASSETS) { const filePath = path.join(DIST, file); const sizeMB = (fs.statSync(filePath).size / 1024 / 1024).toFixed(1); process.stdout.write(` Upload ${file} (${sizeMB} MB)...`); await uploadAsset(release.id, filePath); console.log(' OK'); } console.log(`\n Release disponible : https://${HOST}/${OWNER}/${REPO}/releases/tag/${TAG}\n`); } main().catch(e => { console.error('\nErreur :', e.message); process.exit(1); });