mirror of
https://gitlab.com/vyla-entertainment/home.git
synced 2026-11-09 19:57:20 -05:00
403 lines
12 KiB
JavaScript
403 lines
12 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
const AdmZip = require('adm-zip');
|
|
const { execSync } = require('child_process');
|
|
|
|
const BUILD_DIR = path.join(__dirname, 'dist-build');
|
|
const REPOS = ['frontend', 'player'];
|
|
|
|
const TEMP_EXTRACT_DIR = path.join(__dirname, 'temp-validation-extract');
|
|
|
|
const GITLAB_GROUP = 'vyla-entertainment';
|
|
|
|
const REPO_CONFIG = {
|
|
'frontend': { name: 'frontend', branch: 'main' },
|
|
'player': { name: 'player', branch: 'main' },
|
|
};
|
|
|
|
const clonedRepos = new Set();
|
|
|
|
function clonePrivateRepo(repoConfig, targetDir) {
|
|
if (fs.existsSync(targetDir)) {
|
|
console.log(`Removing stale ${repoConfig.name} before fresh clone...`);
|
|
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
}
|
|
|
|
const token = process.env.GITLAB_TOKEN || process.env.VYLA_GITLAB_TOKEN;
|
|
const auth = token ? `oauth2:${token}@` : '';
|
|
const repoUrl = `https://${auth}gitlab.com/${GITLAB_GROUP}/${repoConfig.name}.git`;
|
|
|
|
try {
|
|
execSync(`git clone --depth 1 --branch ${repoConfig.branch} ${repoUrl} ${targetDir}`, {
|
|
stdio: 'inherit'
|
|
});
|
|
clonedRepos.add(repoConfig.name);
|
|
} catch (error) {
|
|
throw new Error(`Failed to clone ${repoConfig.name}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
function cloneAllRepos() {
|
|
for (const [key, config] of Object.entries(REPO_CONFIG)) {
|
|
const targetDir = path.join(__dirname, key);
|
|
clonePrivateRepo(config, targetDir);
|
|
}
|
|
}
|
|
|
|
async function installDependencies() {
|
|
await Promise.all(REPOS.map(repo => installOneRepo(repo)));
|
|
}
|
|
|
|
function installOneRepo(repo) {
|
|
const repoDir = path.join(__dirname, repo);
|
|
const pkgJsonPath = path.join(repoDir, 'package.json');
|
|
|
|
if (!fs.existsSync(pkgJsonPath)) {
|
|
return;
|
|
}
|
|
try {
|
|
execSync('npm install --omit=dev', {
|
|
cwd: repoDir,
|
|
stdio: 'inherit'
|
|
});
|
|
} catch (error) {
|
|
throw new Error(`npm install failed for ${repo}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
function cleanBuildDir() {
|
|
if (fs.existsSync(BUILD_DIR)) {
|
|
fs.rmSync(BUILD_DIR, { recursive: true, force: true });
|
|
}
|
|
fs.mkdirSync(BUILD_DIR, { recursive: true });
|
|
}
|
|
|
|
let credentialsId = null;
|
|
|
|
function encryptCredentialsFile() {
|
|
const CryptoUtils = require('./crypto-utils');
|
|
|
|
const credentialsTemplate = path.join(__dirname, 'config/credentials-template.json');
|
|
const encryptedOutput = path.join(__dirname, 'config/encrypted-credentials.json');
|
|
|
|
if (!fs.existsSync(credentialsTemplate)) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const credentials = JSON.parse(fs.readFileSync(credentialsTemplate, 'utf8'));
|
|
|
|
const hasPlaceholders = Object.values(credentials).some(service =>
|
|
Object.values(service).some(value =>
|
|
value && value.includes('YOUR_') && value.includes('_HERE')
|
|
)
|
|
);
|
|
|
|
if (hasPlaceholders) {
|
|
const envCredentials = {};
|
|
for (const [service, keys] of Object.entries(credentials)) {
|
|
envCredentials[service] = {};
|
|
for (const key of Object.keys(keys)) {
|
|
const envVarName = `${service.toUpperCase().replace(/-/g, '_')}_${key.toUpperCase()}`;
|
|
if (process.env[envVarName]) {
|
|
envCredentials[service][key] = process.env[envVarName];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Object.values(envCredentials).some(s => Object.keys(s).length > 0)) {
|
|
Object.assign(credentials, envCredentials);
|
|
}
|
|
}
|
|
|
|
credentialsId = crypto.createHash('sha256').update(JSON.stringify(credentials)).digest('hex');
|
|
const buildKey = CryptoUtils.deriveKey('vyla-home-build-key', 'vyla-home-payload-salt-v1');
|
|
const encryptedData = CryptoUtils.encryptObject(credentials, buildKey);
|
|
|
|
fs.writeFileSync(encryptedOutput, encryptedData, 'utf8');
|
|
|
|
return encryptedOutput;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function collectJsFiles(src, dest, out) {
|
|
const files = fs.readdirSync(src);
|
|
fs.mkdirSync(dest, { recursive: true });
|
|
|
|
for (const file of files) {
|
|
const srcPath = path.join(src, file);
|
|
const destPath = path.join(dest, file);
|
|
const stat = fs.statSync(srcPath);
|
|
|
|
if (stat.isDirectory()) {
|
|
if (file === 'node_modules' || file === '.git' || file === '.github') continue;
|
|
collectJsFiles(srcPath, destPath, out);
|
|
} else if (stat.isFile()) {
|
|
fs.copyFileSync(srcPath, destPath);
|
|
if (path.extname(srcPath).toLowerCase() === '.js') {
|
|
out.push(destPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function processFolder(src, dest) {
|
|
const jsFiles = [];
|
|
collectJsFiles(src, dest, jsFiles);
|
|
|
|
let isEsm = false;
|
|
const pkgPath = path.join(src, 'package.json');
|
|
if (fs.existsSync(pkgPath)) {
|
|
try {
|
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
if (pkg.type === 'module') isEsm = true;
|
|
} catch (e) { }
|
|
}
|
|
}
|
|
|
|
function getFileChecksum(filePath) {
|
|
const hash = crypto.createHash('sha256');
|
|
const data = fs.readFileSync(filePath);
|
|
hash.update(data);
|
|
return hash.digest('hex');
|
|
}
|
|
|
|
function validatePayload(zipPath) {
|
|
if (fs.existsSync(TEMP_EXTRACT_DIR)) {
|
|
fs.rmSync(TEMP_EXTRACT_DIR, { recursive: true, force: true });
|
|
}
|
|
fs.mkdirSync(TEMP_EXTRACT_DIR, { recursive: true });
|
|
|
|
const zip = new AdmZip(zipPath);
|
|
zip.extractAllTo(TEMP_EXTRACT_DIR, true);
|
|
|
|
const requiredFiles = [
|
|
'frontend/index.html'
|
|
];
|
|
|
|
const validationResults = {
|
|
valid: true,
|
|
errors: [],
|
|
checksum: getFileChecksum(zipPath),
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
for (const file of requiredFiles) {
|
|
const fullPath = path.join(TEMP_EXTRACT_DIR, file);
|
|
if (!fs.existsSync(fullPath)) {
|
|
validationResults.valid = false;
|
|
validationResults.errors.push(`Missing critical file: ${file}`);
|
|
}
|
|
}
|
|
|
|
const envPath = path.join(__dirname, 'config/env.json');
|
|
const replacementsPath = path.join(__dirname, 'config/replacements.json');
|
|
|
|
if (!fs.existsSync(envPath)) {
|
|
validationResults.valid = false;
|
|
validationResults.errors.push('Missing environment configuration: config/env.json');
|
|
} else {
|
|
try {
|
|
JSON.parse(fs.readFileSync(envPath, 'utf8'));
|
|
} catch (e) {
|
|
validationResults.valid = false;
|
|
validationResults.errors.push(`Invalid JSON format in config/env.json: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
if (!fs.existsSync(replacementsPath)) {
|
|
validationResults.valid = false;
|
|
validationResults.errors.push('Missing replacements configuration: config/replacements.json');
|
|
} else {
|
|
try {
|
|
JSON.parse(fs.readFileSync(replacementsPath, 'utf8'));
|
|
} catch (e) {
|
|
validationResults.valid = false;
|
|
validationResults.errors.push(`Invalid JSON format in config/replacements.json: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
fs.rmSync(TEMP_EXTRACT_DIR, { recursive: true, force: true });
|
|
|
|
fs.writeFileSync(path.join(__dirname, 'validation-report.json'), JSON.stringify(validationResults, null, 2), 'utf8');
|
|
|
|
if (!validationResults.valid) {
|
|
console.error("Payload validation failed with errors:", validationResults.errors);
|
|
process.exit(1);
|
|
}
|
|
|
|
return validationResults.checksum;
|
|
}
|
|
|
|
function directoryId(dir) {
|
|
const hash = crypto.createHash('sha256');
|
|
const walk = (current, rel) => {
|
|
for (const name of fs.readdirSync(current).sort()) {
|
|
const full = path.join(current, name);
|
|
const relPath = rel ? `${rel}/${name}` : name;
|
|
if (fs.statSync(full).isDirectory()) {
|
|
walk(full, relPath);
|
|
} else {
|
|
hash.update(`${relPath}\0${getFileChecksum(full)}\n`);
|
|
}
|
|
}
|
|
};
|
|
walk(dir, '');
|
|
return hash.digest('hex');
|
|
}
|
|
|
|
function signManifest(manifest) {
|
|
const key = process.env.VYLA_UPDATE_SIGNING_KEY;
|
|
if (!key) {
|
|
throw new Error('VYLA_UPDATE_SIGNING_KEY is required to sign the update manifest.');
|
|
}
|
|
const UpdateClient = require('./update-client');
|
|
return crypto.sign(null, Buffer.from(UpdateClient.canonicalManifest(manifest)), key.replace(/\\n/g, '\n')).toString('base64');
|
|
}
|
|
|
|
async function build() {
|
|
try {
|
|
cloneAllRepos();
|
|
} catch (error) {
|
|
console.error("Cloning failed:", error);
|
|
process.exit(1);
|
|
}
|
|
|
|
for (const repo of REPOS) {
|
|
const srcRepo = path.join(__dirname, repo);
|
|
if (!fs.existsSync(srcRepo)) {
|
|
console.error(`Repository directory missing: ${srcRepo}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
try {
|
|
await installDependencies();
|
|
} catch (error) {
|
|
console.error("Dependency installation failed:", error);
|
|
process.exit(1);
|
|
}
|
|
|
|
let encryptedCredentialsPath = null;
|
|
try {
|
|
encryptedCredentialsPath = encryptCredentialsFile();
|
|
} catch (error) {
|
|
console.error("Credential encryption warning (non-fatal):", error);
|
|
}
|
|
|
|
cleanBuildDir();
|
|
|
|
for (const repo of REPOS) {
|
|
const srcRepo = path.join(__dirname, repo);
|
|
const destRepo = path.join(BUILD_DIR, repo);
|
|
processFolder(srcRepo, destRepo);
|
|
|
|
const srcModules = path.join(srcRepo, 'node_modules');
|
|
const destModules = path.join(destRepo, 'node_modules');
|
|
if (fs.existsSync(srcModules)) {
|
|
fs.cpSync(srcModules, destModules, {
|
|
recursive: true,
|
|
filter: (src) => !/[\\/](test|tests|docs|\.github|\.git)([\\/]|$)/.test(src) && !/\.(md|markdown|map|ts)$/i.test(src)
|
|
});
|
|
}
|
|
}
|
|
|
|
if (encryptedCredentialsPath && fs.existsSync(encryptedCredentialsPath)) {
|
|
const credentialsDest = path.join(BUILD_DIR, 'encrypted-credentials.json');
|
|
fs.copyFileSync(encryptedCredentialsPath, credentialsDest);
|
|
}
|
|
|
|
const configDir = path.join(BUILD_DIR, 'config');
|
|
fs.mkdirSync(configDir, { recursive: true });
|
|
|
|
const envConfig = path.join(__dirname, 'config/env.json');
|
|
const replacementsConfig = path.join(__dirname, 'config/replacements.json');
|
|
|
|
if (fs.existsSync(envConfig)) {
|
|
fs.copyFileSync(envConfig, path.join(configDir, 'env.json'));
|
|
}
|
|
if (fs.existsSync(replacementsConfig)) {
|
|
fs.copyFileSync(replacementsConfig, path.join(configDir, 'replacements.json'));
|
|
}
|
|
|
|
const zip = new AdmZip();
|
|
for (const repo of REPOS) {
|
|
const destRepo = path.join(BUILD_DIR, repo);
|
|
zip.addLocalFolder(destRepo, repo);
|
|
}
|
|
|
|
zip.addLocalFolder(configDir, 'config');
|
|
|
|
if (fs.existsSync(path.join(BUILD_DIR, 'encrypted-credentials.json'))) {
|
|
zip.addFile('encrypted-credentials.json', fs.readFileSync(path.join(BUILD_DIR, 'encrypted-credentials.json')));
|
|
}
|
|
|
|
const zipPath = path.join(__dirname, 'update-payload.zip');
|
|
if (fs.existsSync(zipPath)) {
|
|
fs.rmSync(zipPath, { force: true });
|
|
}
|
|
zip.writeZip(zipPath);
|
|
|
|
const checksum = validatePayload(zipPath);
|
|
|
|
let wrapperVersion = '2.0.0';
|
|
try {
|
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
|
|
wrapperVersion = pkg.version || '2.0.0';
|
|
} catch (e) { }
|
|
const releaseVersion = (process.env.RELEASE_VERSION || wrapperVersion).replace(/^v/, '');
|
|
|
|
const assetsDir = path.join(__dirname, 'release-assets');
|
|
fs.rmSync(assetsDir, { recursive: true, force: true });
|
|
fs.mkdirSync(assetsDir, { recursive: true });
|
|
|
|
const components = {};
|
|
const addComponent = (name, fill, id) => {
|
|
const file = `${name}.zip`;
|
|
const target = path.join(assetsDir, file);
|
|
const componentZip = new AdmZip();
|
|
fill(componentZip);
|
|
componentZip.writeZip(target);
|
|
components[name] = { file, sha256: getFileChecksum(target), id, size: fs.statSync(target).size };
|
|
};
|
|
|
|
for (const repo of REPOS) {
|
|
const destRepo = path.join(BUILD_DIR, repo);
|
|
addComponent(repo, (z) => z.addLocalFolder(destRepo, repo), directoryId(destRepo));
|
|
}
|
|
addComponent('config', (z) => z.addLocalFolder(configDir, 'config'), directoryId(configDir));
|
|
|
|
const bundledCredentials = path.join(BUILD_DIR, 'encrypted-credentials.json');
|
|
if (fs.existsSync(bundledCredentials) && credentialsId) {
|
|
addComponent('secrets', (z) => z.addFile('encrypted-credentials.json', fs.readFileSync(bundledCredentials)), credentialsId);
|
|
}
|
|
|
|
const manifest = {
|
|
version: releaseVersion,
|
|
sha256: checksum,
|
|
timestamp: new Date().toISOString(),
|
|
hasEncryptedCredentials: !!encryptedCredentialsPath,
|
|
components
|
|
};
|
|
manifest.signature = signManifest(manifest);
|
|
|
|
fs.writeFileSync(path.join(__dirname, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
|
|
|
|
for (const repo of REPOS) {
|
|
if (clonedRepos.has(repo)) {
|
|
const repoPath = path.join(__dirname, repo);
|
|
if (fs.existsSync(repoPath)) {
|
|
fs.rmSync(repoPath, { recursive: true, force: true });
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
build().catch((error) => {
|
|
console.error("Build process encountered an unhandled exception:", error);
|
|
process.exit(1);
|
|
}); |