@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
JavaScript
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('/');
}