/* Creates a complete release bundle for fast restore: - Tags frontend and backend repos with an annotated tag - Copies code snapshots (tar.gz) for frontend and backend - Dumps MongoDB collections to JSON (users, sourcetexts, submissions, subtitles, subtitlesubmissions) - Writes a manifest.json with commit SHAs, tag, counts, and timestamps Usage: node create-release-bundle.js [--tag TAG_NAME] Requires: MONGODB_URI env (or falls back to local), git available */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const mongoose = require('mongoose'); const ROOT = path.resolve(__dirname, '..', '..'); const BACKEND_DIR = path.resolve(__dirname); const FRONTEND_DIR = path.resolve(__dirname, '../frontend'); const BACKUPS_DIR = path.join(BACKEND_DIR, 'backups'); const RELEASES_DIR = path.join(BACKUPS_DIR, 'releases'); const argTag = process.argv.includes('--tag') ? process.argv[process.argv.indexOf('--tag') + 1] : null; const ts = new Date().toISOString().replace(/[:.]/g, '-'); const tagName = argTag || `prod-${ts}`; function ensureDir(p) { fs.mkdirSync(p, { recursive: true }); } function getGitInfo(repoDir) { const sha = execSync('git rev-parse HEAD', { cwd: repoDir }).toString().trim(); const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: repoDir }).toString().trim(); return { sha, branch }; } function tagAndPush(repoDir, tag) { try { execSync(`git tag -a ${tag} -m "Release ${tag}"`, { cwd: repoDir, stdio: 'inherit' }); } catch (e) { // if tag exists, continue } // Prefer pushing to 'huggingface' remote if present, else default try { const remotes = execSync('git remote', { cwd: repoDir }).toString().split('\n').map(r => r.trim()).filter(Boolean); if (remotes.includes('huggingface')) { execSync('git push --tags huggingface', { cwd: repoDir, stdio: 'inherit' }); } else { execSync('git push --tags', { cwd: repoDir, stdio: 'inherit' }); } } catch (e) { console.warn('āš ļø Warning: failed to push tags for', repoDir, '- continuing'); } } async function dumpCollections(outDir) { const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017/transcreation-sandbox'; await mongoose.connect(uri, { serverSelectionTimeoutMS: 5000 }); const conn = mongoose.connection; const collections = ['users', 'sourcetexts', 'submissions', 'subtitles', 'subtitlesubmissions']; const counts = {}; for (const name of collections) { try { const docs = await conn.db.collection(name).find({}).toArray(); fs.writeFileSync(path.join(outDir, `${name}.json`), JSON.stringify(docs, null, 2)); counts[name] = docs.length; } catch (e) { counts[name] = 0; } } await mongoose.disconnect(); return counts; } (async () => { console.log('šŸ“¦ Creating release bundle...'); ensureDir(RELEASES_DIR); const bundleDir = path.join(RELEASES_DIR, `release-${ts}`); ensureDir(bundleDir); // Git info and tags console.log('šŸ”– Tagging repositories...'); const feInfo = getGitInfo(FRONTEND_DIR); const beInfo = getGitInfo(BACKEND_DIR); tagAndPush(FRONTEND_DIR, tagName); tagAndPush(BACKEND_DIR, tagName); // Code archives console.log('šŸ—œļø Archiving code...'); const feTar = path.join(bundleDir, `frontend-${feInfo.sha.slice(0,7)}.tar.gz`); const beTar = path.join(bundleDir, `backend-${beInfo.sha.slice(0,7)}.tar.gz`); execSync(`tar -czf "${feTar}" -C "${FRONTEND_DIR}" .`); execSync(`tar -czf "${beTar}" -C "${BACKEND_DIR}" .`); // DB dump console.log('šŸ’¾ Dumping database...'); const dbDir = path.join(bundleDir, 'db'); ensureDir(dbDir); const counts = await dumpCollections(dbDir); // Manifest const manifest = { createdAt: new Date().toISOString(), tag: tagName, frontend: feInfo, backend: beInfo, archives: { frontend: path.basename(feTar), backend: path.basename(beTar) }, db: { path: 'db', counts } }; fs.writeFileSync(path.join(bundleDir, 'manifest.json'), JSON.stringify(manifest, null, 2)); console.log('\nšŸŽ‰ Release bundle created'); console.log('šŸ“ Location:', bundleDir); console.log('šŸ”– Tag:', tagName); })();