#!/usr/bin/env node import { execSync } from 'child_process'; import { readFileSync, writeFileSync, statSync, createReadStream, existsSync } from 'fs'; import { resolve, basename } from 'path'; const ROOT = resolve(import.meta.dirname, '..'); const PKG_PATH = resolve(ROOT, 'package.json'); const RELEASE_DIR = resolve(ROOT, 'release'); const PRODUCT_NAME = 'Multi-Hoster-Upload'; // --- CLI args --- const args = process.argv.slice(2); const dryRun = args.includes('--dry-run'); const version = args.find(a => /^\d+\.\d+\.\d+$/.test(a)); const notes = args.filter(a => a !== version && a !== '--dry-run').join(' ') || ''; if (!version) { console.error('Usage: node scripts/release_gitea.mjs [release notes] [--dry-run]'); console.error('Example: node scripts/release_gitea.mjs 1.0.1 "Bugfix release"'); process.exit(1); } const tag = `v${version}`; // --- Helpers --- function run(cmd, opts = {}) { console.log(` $ ${cmd}`); if (dryRun && !opts.allowDry) { console.log(' [dry-run] skipped'); return ''; } const output = execSync(cmd, { cwd: ROOT, encoding: 'utf-8', stdio: opts.stdio || 'pipe' }); return typeof output === 'string' ? output.trim() : ''; } function resolveGiteaRemote() { const remotes = run('git remote -v', { allowDry: true }); for (const name of ['gitea', 'forgejo', 'origin']) { const match = remotes.match(new RegExp(`^${name}\\s+(\\S+)`, 'm')); if (match && match[1].includes('git.24-music.de')) return { name, url: match[1] }; } return null; } function resolveToken() { // Env var first const envToken = process.env.GITEA_TOKEN || process.env.FORGEJO_TOKEN; if (envToken) return envToken; // Try git credential helper try { const input = 'protocol=https\nhost=git.24-music.de\n\n'; const output = execSync('git credential fill', { input, encoding: 'utf-8', cwd: ROOT }); const match = output.match(/password=(.+)/); if (match) return match[1].trim(); } catch {} return null; } async function giteaApi(method, urlPath, token, body) { const base = process.env.GITEA_BASE_URL || 'https://git.24-music.de'; const url = `${base}${urlPath}`; const headers = { 'Authorization': `token ${token}`, 'Content-Type': 'application/json' }; const res = await fetch(url, { method, headers, body: body ? JSON.stringify(body) : undefined }); const text = await res.text(); let json; try { json = JSON.parse(text); } catch { json = null; } if (!res.ok && res.status !== 409 && res.status !== 422) { throw new Error(`Gitea API ${res.status}: ${text.slice(0, 300)}`); } return { status: res.status, data: json }; } async function uploadAsset(releaseId, filePath, token) { const base = process.env.GITEA_BASE_URL || 'https://git.24-music.de'; const name = basename(filePath); const size = statSync(filePath).size; const url = `${base}/api/v1/repos/Administrator/${PRODUCT_NAME}/releases/${releaseId}/assets?name=${encodeURIComponent(name)}`; console.log(` Uploading ${name} (${(size / 1024 / 1024).toFixed(1)} MB)...`); if (dryRun) { console.log(' [dry-run] skipped'); return; } const stream = createReadStream(filePath); const res = await fetch(url, { method: 'POST', headers: { 'Authorization': `token ${token}`, 'Content-Type': 'application/octet-stream', 'Content-Length': String(size) }, body: stream, duplex: 'half' }); if (!res.ok) { const text = await res.text(); throw new Error(`Asset upload failed (${res.status}): ${text.slice(0, 200)}`); } console.log(` Uploaded: ${name}`); } // --- Main --- async function main() { console.log(`\nReleasing ${PRODUCT_NAME} ${tag}${dryRun ? ' [DRY RUN]' : ''}\n`); // 1. Resolve remote const remote = resolveGiteaRemote(); if (!remote) { console.error('No Gitea remote found. Add one: git remote add gitea https://git.24-music.de/Administrator/Multi-Hoster-Upload.git'); process.exit(1); } console.log(`Remote: ${remote.name} -> ${remote.url}`); // 2. Check clean working tree const status = run('git status --porcelain', { allowDry: true }); const trackedChanges = status.split('\n').filter(l => l.trim() && !l.startsWith('??')).join('\n'); if (trackedChanges) { console.error('Working tree has uncommitted tracked changes. Commit or stash first.'); process.exit(1); } // 3. Check if tag already exists (recovery mode) let recoveryMode = false; try { run(`git rev-parse ${tag}`, { allowDry: true }); console.log(`Tag ${tag} already exists - recovery mode (skip version bump/git)`); recoveryMode = true; } catch {} if (!recoveryMode) { // 4. Update package.json version const pkg = JSON.parse(readFileSync(PKG_PATH, 'utf-8')); pkg.version = version; if (!dryRun) writeFileSync(PKG_PATH, JSON.stringify(pkg, null, 2) + '\n', 'utf-8'); console.log(`Updated package.json -> ${version}`); // 5. Build console.log('\nBuilding...'); run('npm run release:win', { stdio: 'inherit' }); // 6. Git commit + tag + push run('git add package.json'); run(`git commit -m "release: ${tag}"`); run(`git tag ${tag}`); run(`git push ${remote.name} HEAD`); run(`git push ${remote.name} ${tag}`); } // 7. Verify artifacts const expectedArtifacts = [ `${PRODUCT_NAME} Setup ${version}.exe`, `${PRODUCT_NAME} ${version}.exe`, 'latest.yml' ]; for (const name of expectedArtifacts) { const p = resolve(RELEASE_DIR, name); if (!existsSync(p)) { console.error(`Missing artifact: ${p}`); process.exit(1); } } // Also check for blockmap const blockmapName = `${PRODUCT_NAME} Setup ${version}.exe.blockmap`; const hasBlockmap = existsSync(resolve(RELEASE_DIR, blockmapName)); console.log('\nArtifacts verified.'); // 8. Get token const token = resolveToken(); if (!token) { console.error('No Gitea token. Set GITEA_TOKEN env var or configure git credential helper.'); process.exit(1); } // 9. Create release const releaseBody = notes || `${PRODUCT_NAME} ${tag}`; let releaseId; const { status: createStatus, data: createData } = await giteaApi( 'POST', `/api/v1/repos/Administrator/${PRODUCT_NAME}/releases`, token, { tag_name: tag, name: `${PRODUCT_NAME} ${tag}`, body: releaseBody } ); if (createStatus === 409 || createStatus === 422) { // Release already exists, find it const { data: releases } = await giteaApi('GET', `/api/v1/repos/Administrator/${PRODUCT_NAME}/releases/tags/${tag}`, token); releaseId = releases.id; console.log(`Release already exists (id: ${releaseId})`); } else { releaseId = createData.id; console.log(`Created release (id: ${releaseId})`); } // 10. Upload assets for (const name of expectedArtifacts) { await uploadAsset(releaseId, resolve(RELEASE_DIR, name), token); } if (hasBlockmap) { await uploadAsset(releaseId, resolve(RELEASE_DIR, blockmapName), token); } console.log(`\nDone! Release: ${process.env.GITEA_BASE_URL || 'https://git.24-music.de'}/Administrator/${PRODUCT_NAME}/releases/tag/${tag}\n`); } main().catch(err => { console.error('\nRelease failed:', err.message); process.exit(1); });