/* eslint-disable @typescript-eslint/no-var-requires */ /** * npm run release:build [--dry-run] [--stable] * * --dry-run don't commit CHANGELOG.md * --stable generate changelog * */ const colors = require('colors'); const fs = require('fs'); const path = require('path'); const libxml = require('libxmljs'); const https = require('https'); const archiver = require('archiver'); const simpleGit = require('simple-git'); const inquirer = require('inquirer'); const { exec } = require('child_process'); const { generateChangelog, hasChangeLogEntry } = require('./imports/changelog'); const packageInfo = require('../package.json'); colors.setTheme({ verbose: 'cyan', warn: 'yellow', error: 'red', }); const git = simpleGit(); const infoXmlPath = './appinfo/info.xml'; const isStableRelease = process.argv.indexOf('--stable') > 1; const isDryRun = process.argv.indexOf('--dry-run') > 1; async function getVersion() { return packageInfo.version + (!isStableRelease ? '-git.' + (await git.raw(['rev-parse', '--short', 'HEAD'])).trim() : ''); } run().catch(err => { console.log(`✘ ${err.toString()}`.error); }); async function run() { const appId = await prepareInfoXml(); await createRelease(appId); } async function prepareInfoXml() { const infoFile = fs.readFileSync(infoXmlPath); const xmlDoc = libxml.parseXml(infoFile); updateVersion(xmlDoc, await getVersion()); await validateXml(xmlDoc); return xmlDoc.get('//id').text(); } function updateVersion(xmlDoc, version) { const versionChild = xmlDoc.get('//version'); const currentVersion = versionChild.text(); if (version !== currentVersion) { console.log(`✔ Update version in info.xml to ${version}.`.green); versionChild.text(version); fs.writeFileSync(infoXmlPath, xmlDoc.toString()); } } async function createRelease(appId) { const version = await getVersion(); console.log(`I'm now building ${appId} in version ${version}.`.verbose); if (isStableRelease) { await isRepoClean(); console.log('✔ repo is clean'.green); } const execa = (await import('execa')).execaCommand; const composerDev = await execa('composer install'); console.log(composerDev.stdout, composerDev.stderr); console.log('✔ composer dev dependencies installed'.green); await execa('yarn lint'); console.log('✔ linters are happy'.green); const composerNoDev = await execa('composer install --no-dev'); console.log(composerNoDev.stdout, composerNoDev.stderr); console.log('✔ composer dependencies installed'.green); await execa('yarn build'); console.log('✔ webpack done'.green); await updateChangelog(); const filePath = await createArchive(appId, appId + '-v' + version); await createNextcloudSignature(appId, filePath); await createGPGSignature(filePath); await createGPGArmorSignature(filePath); await execa('composer install'); console.log('✔ composer dev dependencies installed'.green); } async function isRepoClean() { const status = await git.status(); if (status.staged.length > 1) { throw 'Repo not clean. Found staged files.'; } if (status.modified.length > 2 || !status.modified.includes('package.json') || !status.modified.includes('appinfo/info.xml')) { throw 'Repo not clean. Found modified files.'; } if (status.not_added.length > 0) { throw 'Repo not clean. Found not added files.'; } } async function updateChangelog() { if (!isStableRelease) { console.log('Skip changelog for non-stable releases.'.warn); return; } const changeLog = await generateChangelog(packageInfo.version); console.log('✔ change log generated'.green); console.log(changeLog); console.log('Press any key to continue...'); await keypress(); await hasChangeLogEntry(packageInfo.version); console.log('✔ there is a change log entry for this version'.green); await commitChangeLog(); console.log('✔ change log commited'.green); } async function keypress() { return inquirer.prompt([{ type: 'input', name: 'keypress', message: 'Press any key to continue... (where is the any key?)', }]); } async function commitChangeLog() { const status = await git.status(); if (status.staged.length > 0) { throw 'Repo not clean. Found staged files.'; } if (!isDryRun) { await git.add('CHANGELOG.md'); await git.commit('docs: update change log', ['-n']); } } function createArchive(appId, fileBaseName) { const archivesPath = path.normalize(__dirname + '/../archives/'); if (!fs.existsSync(archivesPath)){ fs.mkdirSync(archivesPath); } const fileName = `${fileBaseName}.tar.gz`; const filePath = path.normalize(__dirname + `/../archives/${fileName}`); const output = fs.createWriteStream(filePath); const archive = archiver('tar', { gzip: true, }); archive.on('warning', function (err) { if (err.code === 'ENOENT') { console.warn('Archive warning: '.warn, err); } else { throw err; } }); archive.on('error', function (err) { throw err; }); archive.pipe(output); function addDirectory(name) { archive.directory(name + '/', `${appId}/${name}/`); } function addFile(name) { archive.file(name, { name: `${appId}/${name}` }); } addDirectory('appinfo'); addDirectory('img'); addDirectory('js'), addDirectory('l10n'), addDirectory('lib'); addDirectory('templates'); addFile('COPYING'); addFile('README.md'); addFile('CHANGELOG.md'); archive.glob('vendor/**/*', { ignore: ['.git'], }, { prefix: appId, }); return new Promise(resolve => { output.on('close', function () { console.log(`✔ Wrote ${archive.pointer()} bytes to ${fileName}`.green); resolve(filePath); }); archive.finalize(); }); } function createNextcloudSignature(appId, filePath) { return new Promise((resolve) => { const sigPath = filePath + '.ncsig'; exec(`openssl dgst -sha512 -sign ~/.nextcloud/certificates/${appId}.key ${filePath} | openssl base64 > ${sigPath}`, (error, stdout, stderr) => { if (error) { throw error; } if (stdout) { console.log(`stdout: ${stdout}`); } if (stderr) { console.log(`stderr: ${stderr}`); } console.log(`✔ created Nextcloud signature: ${path.basename(sigPath)}`.green); resolve(); }); }); } function createGPGSignature(filePath) { return new Promise((resolve) => { exec(`gpg --yes --detach-sign "${filePath}"`, (error, stdout, stderr) => { if (error) { throw error; } if (stdout) { console.log(`stdout: ${stdout}`); } if (stderr) { console.log(`stderr: ${stderr}`); } console.log(`✔ created detached signature: ${path.basename(filePath)}.sig`.green); resolve(); }); }); } function createGPGArmorSignature(filePath) { return new Promise((resolve) => { exec(`gpg --yes --detach-sign --armor "${filePath}"`, (error, stdout, stderr) => { if (error) { throw error; } if (stdout) { console.log(`stdout: ${stdout}`); } if (stderr) { console.log(`stderr: ${stderr}`); } console.log(`✔ created detached signature: ${path.basename(filePath)}.asc`.green); resolve(); }); }); } async function validateXml(xmlDoc) { const schemaLocation = xmlDoc.root().attr('noNamespaceSchemaLocation').value(); if (!schemaLocation) { throw 'Found no schema location'; } let schemaString; try { console.log('Downloading schema file...'.verbose); schemaString = await wget(schemaLocation); } catch (err) { console.log('Could not download schema. Skip validation.'.warn); return; } const xsdDoc = libxml.parseXml(schemaString); if (xmlDoc.validate(xsdDoc)) { console.log('✔ Document valid'.green); } else { console.log('✘ Document INVALID'.error); xmlDoc.validationErrors.forEach((error, index) => { console.log(`#${index + 1}\t${error.toString().trim()}`.warn); console.log(`\tLine ${error.line}:${error.column} (level ${error.level})`.verbose); }); throw 'Abort'; } } function wget(url) { return new Promise((resolve, reject) => { https.get(url, (resp) => { let data = ''; resp.on('data', (chunk) => { data += chunk; }); resp.on('end', () => { resolve(data); }); }).on('error', (err) => { reject(err); }); }); }