feat: add Gitea release automation script

This commit is contained in:
Administrator 2026-03-10 02:35:25 +01:00
parent d75eaea2fc
commit b0b66c165d

219
scripts/release_gitea.mjs Normal file
View File

@ -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 <version> [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);
});