diff --git a/scripts/release_gitea.mjs b/scripts/release_gitea.mjs new file mode 100644 index 0000000..b2b99ac --- /dev/null +++ b/scripts/release_gitea.mjs @@ -0,0 +1,219 @@ +#!/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 ''; } + return execSync(cmd, { cwd: ROOT, encoding: 'utf-8', stdio: opts.stdio || 'pipe' }).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); +});