UNPKG

@tangelo/tangelo-configuration-toolkit

Version:

Tangelo Configuration Toolkit is a command-line toolkit which offers support for developing a Tangelo configuration.

206 lines (171 loc) 8.68 kB
const del = require('del'); const gulp = require('gulp'); const path = require('path'); const RemoteExec = require('../../lib/remote-exec'); const sftpClient = require('ssh2-sftp-client'); const tinylr = require('tiny-lr'); const transfer = require('./execute'); const c = require('./config'); const s = require('./srcset'); const inquirer = require('inquirer'); const checkDeployment = async ftpConfig => { const deployToDevServer = /\.dev\./.test( ftpConfig?.host ); const deployToTestServer = /\.t(e)?st\./.test( ftpConfig?.host ); const deployToProdServer = c?.deliveryPack || !( deployToDevServer || deployToTestServer ); const isBehind = _git.commitLocal().date < _git.commitRemote().date; // In most cases productionBranches are called 'master' or 'main'; Some customers use a development, test and production branches structure; hence we allow 'producti(on|e)' const isPrdBranch = ( /master|main|producti(on|e)/.test( _git.commitLocal().branch ) ); if (!deployToTestServer && !deployToProdServer) { // dev-environements if (isBehind) { /* Warn for dev deployements that are behind */ _warn(`You're not deploying from the most recent commit in branch "${_git.commitLocal().branch}"! Update your repository: ${'tct git --update-repo'.cyan}`); } return; } // Checks for test / prd environments: if ( !isPrdBranch ) { _warn(`You want to deploy from branch "${_git.commitLocal().branch}" to a production server. It is strongly advised to deploy from the master branch.`); } if (_git.hasUncommittedFiles()) { _warn(`You have uncommitted changes!`); if (_git.commitTdi.hasUncommittedFiles()) _warn(`You have uncommitted changes in the TDI submodule!`); } if ( isBehind ) { _error(`You're not deploying from the most recent commit in branch "${_git.commitLocal().branch}"! Update your repository: ${'tct git --update-repo'.cyan}`); } // Ask for continuation of deployment for production environments only; and only if not deploying from a prd branch or when there exist uncommitted changes if ( (!isPrdBranch || _git.hasUncommittedFiles()) && deployToProdServer && !( await inquirer.prompt( {type: 'confirm', message: 'Are you sure you want to continue?', name: 'confirm', default: false} ).then( res => res.confirm ) ) ) { return {cancel: true}; } }; module.exports = async function deploy (argv) { // common setup c.setServer(argv.server); c.prepareForCopy(argv.filter); const {ftpConfig, remotedir} = c.server; if (!argv.test && ftpConfig) { // only perform deployment checks when actually transferring to remote server const deploy = await checkDeployment(ftpConfig); if (deploy?.cancel) return; } if (ftpConfig) { const logPath = path.join(remotedir, 'log/deployments.log').toFws; const re = new RemoteExec(ftpConfig); ftpConfig.eventPut = file => { _write(file.destination.replace(remotedir, '')); if (path.extname(file.destination)==='.sh') re.add('chmod 755 '+file.destination, 'Permissions set: '+file.destination); else if (file.destination.match(/cmscustom.*hce.*config.xml/)) re.add('touch '+c.getRemotePath('hce/hce-config.xml'), 'Touched: '+c.getRemotePath('hce/hce-config.xml')); else if (file.destination.match(/cmscustom.*od[fts].*config.xml/)) re.add('touch '+c.getRemotePath('odf/odf-config.xml'), 'Touched: '+c.getRemotePath('odf/odf-config.xml')); else if (file.destination.match(/txp\/site-configs/)) re.add('touch '+c.getRemotePath('txp/xmlpages-config.xml'), 'Touched: '+c.getRemotePath('txp/xmlpages-config.xml')); }; ftpConfig.eventBeforeAll = async files => { const dli = deployLogInfo(!argv.copy, c.transferPatterns[0], files, 'START'); await re.add(`echo -e "${dli}" >> ${logPath}`).process(); }; ftpConfig.eventAfterAll = async files => { _info(`${files.length} file(s) transferred`); const dli = deployLogInfo(!argv.copy, c.transferPatterns[0], files, 'DONE'); re.add(`echo -e "${dli}" >> ${logPath}`).add(`echo "$(tail -10000 ${logPath})" > ${logPath}`); await re.process(); }; } // execute chosen option if (argv.test) { s.create([...c.transferPatterns, '!**/fonto/{*.*,!(dist)/**}'], {test: true}); } if (argv.copy) { transfer([...c.transferPatterns, '!**/fonto/{*.*,!(dist)/**}']); } if (argv.watch || argv.live) { if (c?.deliveryPack) _error('You cannot use watch with a delivery-pack.'); _write(`Watching... (press ctrl+c to quit)\n`); const lrServer = argv.live && tinylr(); if (lrServer) lrServer.listen(); const transfers = { queue: [], do () { this.queue = this.queue.map(p => p.replace(/fonto(.)(?:dist|dev).assets.schemas(.sx-shell-.*?).json/, 'fonto$1packages$2$1src$1assets$1schemas$2.json')); // fonto schema changes can be detected in different paths at the same time, rewrite paths to original package-path because files in build folders can be removed before transfer starts this.queue = [...new Set(this.queue)]; // remove duplicates this.queue.forEach(v => s.addToCache(v)); transfer(this.queue, {watch: true, lrServer}); this.queue = []; }, add (fp) { const delay = !fp.match(/fonto/) ? 500 : 1500; // longer delay for fonto build files this.queue.push(fp); clearTimeout(this.delayedExec); this.delayedExec = setTimeout(()=>transfers.do(), delay); } }; // check connection if (ftpConfig) { const sftp = new sftpClient(); sftp.connect(ftpConfig).then(() => sftp.end()).catch(err => _error(`Could not connect to server${err ? ': '+err.message : ''}`)); } gulp.watch(c.transferPatterns) .on('all', (event, filepath) => { if ((event==='add' || event==='change') && ( // within fonto, transfer build files only, but also schema files, because // the "dist" folder isn't watched properly: it does not detect "assets" anymore after building once !/cmscustom.+fonto[\\/]/.test(filepath) || /fonto[\\/]dist[\\/]/.test(filepath) || (/fonto[\\/]dev[\\/]/.test(filepath) && c.envDev) || /fonto[\\/]packages[\\/]sx-shell-.*?[\\/]assets[\\/]schemas[\\/]/.test(filepath) ) ) { transfers.add(filepath); } else if (event==='unlink' && !/fonto/.test(filepath)) { // ignore fonto files s.removeFromCache(filepath); if (!path.parse(filepath).base.match(/\.scss/)) { const rp = c.getRemotePath(filepath); const msg = 'Removed: ' + rp.replace(remotedir, '').white; if (ftpConfig) new RemoteExec(ftpConfig).add(`rm -rf "${rp}"`, msg).process(); else del([rp], {force: true}).then(() => _info(msg, true)); } } }); } }; function deployLogInfo (watch, filter, files, action) { const timestamp = new Date().toISOString(); const user = _git.user().split('@')[0]; const {branch, hash} = _git.commitLocal(); const uncommittedChanges = _git.status().trim() ? ':~' : ''; let logline = `${timestamp} ${action.padEnd(5)} [${user}] [${branch}:${hash.substring(0, 7)}${uncommittedChanges}] [${filter}]`; if (action === 'START') { if (uncommittedChanges) { const uncommittedChanges = _git.status().replace(/\?\?/g, ' U').split('\n'); logline += '\n Uncommitted changes:\n ' + uncommittedChanges.join('\n '); } const filepaths = files.map(({destination}) => destination); logline += `\n Transferring ${filepaths.length} file${filepaths.length > 1 ? 's' : ''}`; if (watch || filepaths.length === 1) logline += ':\n ' + filepaths.join('\n '); else logline += ' in dir:\n ' + getCommonPath(filepaths); } return logline.replace(/"/g, '\\"'); } function getCommonPath(paths) { const splitPaths = paths.map(p => p.split('/')); let commonPath = splitPaths[0]; for (let i = 1; i < splitPaths.length; i++) { const pathParts = splitPaths[i]; commonPath = commonPath.slice(0, Math.min(commonPath.length, pathParts.length)); for (let j = 0; j < commonPath.length; j++) { if (commonPath[j] !== pathParts[j]) { commonPath = commonPath.slice(0, j); break; } } } return commonPath.length === 0 ? '' : commonPath.join('/'); }