theme-utils.mjs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799
  1. import { spawn } from 'child_process';
  2. import fs from 'fs';
  3. import open from 'open';
  4. import inquirer from 'inquirer';
  5. import replace from 'replace-in-file';
  6. const remoteSSH = 'wpcom-sandbox';
  7. const sandboxPublicThemesFolder = '/home/wpdev/public_html/wp-content/themes/pub';
  8. const sandboxRootFolder = '/home/wpdev/public_html/';
  9. const isWin = process.platform === 'win32';
  10. (async function start() {
  11. let args = process.argv.slice(2);
  12. let command = args?.[0];
  13. switch (command) {
  14. case "push-button-deploy-git": return pushButtonDeploy('git');
  15. case "push-button-deploy-svn": return pushButtonDeploy('svn');
  16. case "clean-sandbox-git": return cleanSandboxGit();
  17. case "clean-sandbox-svn": return cleanSandboxSvn();
  18. case "clean-all-sandbox-git": return cleanAllSandboxGit();
  19. case "clean-all-sandbox-svn": return cleanAllSandboxSvn();
  20. case "push-to-sandbox": return pushToSandbox();
  21. case "push-changes-to-sandbox": return pushChangesToSandbox();
  22. case "version-bump-themes": return versionBumpThemes();
  23. case "land-diff-git": return landChangesGit(args?.[1]);
  24. case "land-diff-svn": return landChangesSvn(args?.[1]);
  25. case "deploy-preview": return deployPreview();
  26. case "deploy-theme": return deployThemes([args?.[1]]);
  27. case "build-com-zip": return buildComZip([args?.[1]]);
  28. case "lerna-update-stylecss": return lernaUpdateStyleCss();
  29. }
  30. return showHelp();
  31. })();
  32. function showHelp(){
  33. // TODO: make this helpful
  34. console.log('Help info can go here');
  35. }
  36. /*
  37. Update version number in style.css for each theme
  38. after version bump from conventional commits (via Lerna)
  39. */
  40. async function lernaUpdateStyleCss() {
  41. let newVersion = await executeCommand(`node -p "require('./package.json').version"`);
  42. await replace({
  43. files: './style.css',
  44. from: /(?<=Version:\s*).*?(?=\s*\r?\n|\rg)/gs,
  45. to: ` ${newVersion}`,
  46. });
  47. }
  48. /*
  49. Determine what changes would be deployed
  50. */
  51. async function deployPreview() {
  52. console.clear();
  53. console.log('To ensure accuracy clean your sandbox before previewing. (It is not automatically done).');
  54. console.log('npm run sandbox:clean:git OR npm run sandbox:clean:svn')
  55. let message = await checkForDeployability();
  56. if (message) {
  57. console.log(`\n${message}\n\n`);
  58. }
  59. let hash = await getLastDeployedHash();
  60. console.log(`Last deployed hash: ${hash}`);
  61. let changedThemes = await getChangedThemes(hash);
  62. console.log(`The following themes have changes:\n${changedThemes}`);
  63. let logs = await executeCommand(`git log --reverse --pretty=format:%s ${hash}..HEAD`);
  64. console.log(`\n\nCommit log of changes to be deployed:\n\n${logs}\n\n`);
  65. }
  66. /*
  67. Execute the first phase of a deployment.
  68. Leverages git on the sandbox.
  69. * Gets the last deployed hash from the sandbox
  70. * Version bump all themes have have changes since the last deployment
  71. * Commit the version bump change to github
  72. * Clean the sandbox and ensure it is up-to-date
  73. * Push all changed files (including removal of deleted files) since the last deployment
  74. * Update the 'last deployed' hash on the sandbox
  75. * Create a phabricator diff based on the changes since the last deployment. The description including the commit messages since the last deployment.
  76. * Open the Phabricator Diff in your browser
  77. * Create a tag in the github repository at this point of change which includes the phabricator link in the description
  78. */
  79. async function pushButtonDeploy(repoType) {
  80. console.clear();
  81. let prompt = await inquirer.prompt([{
  82. type: 'confirm',
  83. message: 'You are about to deploy /trunk. Are you ready to continue?',
  84. name: "continue",
  85. default: false
  86. }]);
  87. if(!prompt.continue){
  88. return;
  89. }
  90. if (repoType != 'svn' && repoType != 'git' ) {
  91. return console.log('Specify a repo type to use push-button deploy');
  92. }
  93. let message = await checkForDeployability();
  94. if (message) {
  95. return console.log(`\n\n${message}\n\n`);
  96. }
  97. try {
  98. if (repoType === 'git' ) {
  99. await cleanSandboxGit();
  100. }
  101. else {
  102. await cleanSandboxSvn();
  103. }
  104. let hash = await getLastDeployedHash();
  105. let diffUrl;
  106. await versionBumpThemes();
  107. let changedThemes = await getChangedThemes(hash);
  108. await pushChangesToSandbox();
  109. await updateLastDeployedHash();
  110. if (repoType === 'git' ) {
  111. diffUrl = await createGitPhabricatorDiff(hash);
  112. }
  113. else {
  114. diffUrl = await createSvnPhabricatorDiff(hash);
  115. }
  116. let diffId = diffUrl.split('a8c.com/')[1];
  117. //push changes (from version bump)
  118. await executeCommand('git push');
  119. await tagDeployment({
  120. hash: hash,
  121. diffId: diffId
  122. });
  123. console.log(`\n\nPhase One Complete\n\nYour sandbox has been updated and the diff is available for review.\nPlease give your sandbox a smoke test to determine that the changes work as expected.\nThe following themes have had changes: \n\n${changedThemes.join(' ')}\n\n\n`);
  124. prompt = await inquirer.prompt([{
  125. type: 'confirm',
  126. message: 'Are you ready to land these changes?',
  127. name: "continue",
  128. default: false
  129. }]);
  130. if(!prompt.continue){
  131. console.log(`Aborted Automated Deploy Process Landing Phase\n\nYou will have to land these changes manually. The ID of the diff to land: ${diffId}` );
  132. return;
  133. }
  134. if (repoType === 'git' ) {
  135. await landChangesGit(diffId);
  136. }
  137. else {
  138. await landChangesSvn(diffId);
  139. }
  140. await deployThemes(changedThemes);
  141. await buildComZips(changedThemes);
  142. console.log(`The following themes have changed:\n${changedThemes.join('\n')}`)
  143. console.log('\n\nAll Done!!\n\n');
  144. }
  145. catch (err) {
  146. console.log("ERROR with deply script: ", err);
  147. }
  148. }
  149. /*
  150. Build .zip file for .com
  151. */
  152. async function buildComZip(themeSlug) {
  153. console.log( `Building ${themeSlug} .zip` );
  154. let styleCss = fs.readFileSync(`${themeSlug}/style.css`, 'utf8');
  155. // Gets the theme version (Version:) and minimum WP version (Tested up to:) from the theme's style.css
  156. let themeVersion = getThemeMetadata(styleCss, 'Version');
  157. let wpVersionCompat = getThemeMetadata(styleCss, 'Tested up to');
  158. if (themeVersion && wpVersionCompat) {
  159. await executeOnSandbox(`php ${sandboxRootFolder}bin/themes/theme-downloads/build-theme-zip.php --stylesheet=pub/${themeSlug} --themeversion=${themeVersion} --wpversioncompat=${wpVersionCompat}`, true);
  160. }
  161. else {
  162. console.log('Unable to build theme .zip.');
  163. if (!themeVersion) {
  164. console.log('Could not find theme version (Version:) in the theme style.css.');
  165. }
  166. if (!wpVersionCompat) {
  167. console.log('Could not find WP compat version (Tested up to:) in the theme style.css.');
  168. }
  169. console.log('Please build the .zip file for the theme manually.', themeSlug);
  170. open('https://mc.a8c.com/themes/downloads/');
  171. }
  172. }
  173. async function buildComZips(themes) {
  174. for ( let theme of themes ) {
  175. await buildComZip(theme);
  176. }
  177. }
  178. /*
  179. Check to ensure that:
  180. * The current branch is /trunk
  181. * That trunk is up-to-date with origin/trunk
  182. */
  183. async function checkForDeployability(){
  184. let branchName = await executeCommand('git symbolic-ref --short HEAD');
  185. if(branchName !== 'trunk' ) {
  186. return 'Only the /trunk branch can be deployed.';
  187. }
  188. await executeCommand('git remote update', true);
  189. let localMasterHash = await executeCommand('git rev-parse trunk')
  190. let remoteMasterHash = await executeCommand('git rev-parse origin/trunk')
  191. if(localMasterHash !== remoteMasterHash) {
  192. return 'Local /trunk is out-of-date. Pull changes to continue.'
  193. }
  194. return null;
  195. }
  196. /*
  197. Land the changes from the given diff ID. This is the "production merge".
  198. This is the git version of that action.
  199. */
  200. async function landChangesGit(diffId){
  201. return await executeOnSandbox(`cd ${sandboxPublicThemesFolder};arc patch ${diffId};arc land;exit;`, true, true);
  202. }
  203. /*
  204. Land the changes from the given diff ID. This is the "production merge".
  205. This is the svn version of that action.
  206. */
  207. async function landChangesSvn(diffId){
  208. return await executeOnSandbox(`
  209. cd ${sandboxPublicThemesFolder};
  210. svn ci -m ${diffId}
  211. `, true );
  212. }
  213. async function getChangedThemes(hash) {
  214. console.log('Determining all changed themes');
  215. let themes = await getActionableThemes();
  216. let changedThemes = [];
  217. for (let theme of themes) {
  218. let hasChanges = await checkThemeForChanges(theme, hash);
  219. if(hasChanges){
  220. changedThemes.push(theme);
  221. }
  222. }
  223. return changedThemes;
  224. }
  225. /*
  226. Deploy a collection of themes.
  227. Part of the push-button-deploy process.
  228. Can also be triggered to deploy a single theme with the command:
  229. node ./theme-utils.mjs deploy-theme THEMENAME
  230. */
  231. async function deployThemes( themes ) {
  232. let response;
  233. for ( let theme of themes ) {
  234. console.log( `Deploying ${theme}` );
  235. let deploySuccess = false;
  236. let attempt = 0;
  237. while ( ! deploySuccess) {
  238. attempt++;
  239. console.log(`\nattempt #${attempt}\n\n`);
  240. response = await executeOnSandbox( `deploy pub ${theme};exit;`, true, true );
  241. deploySuccess = response.includes( 'successfully deployed to' );
  242. if( ! deploySuccess ) {
  243. console.log( 'Deploy was not successful. Trying again in 10 seconds...' );
  244. await new Promise(resolve => setTimeout(resolve, 10000));
  245. }
  246. else {
  247. console.log( "Deploy successful." );
  248. }
  249. }
  250. }
  251. }
  252. /*
  253. Provide the hash of the last managed deployment.
  254. This hash is used to determine all the changes that have happened between that point and the current point.
  255. */
  256. async function getLastDeployedHash() {
  257. let result = await executeOnSandbox(`
  258. cat ${sandboxPublicThemesFolder}/.pub-git-hash
  259. `);
  260. return result;
  261. }
  262. /*
  263. Update the 'last deployed hash' on the server with the current hash.
  264. */
  265. async function updateLastDeployedHash() {
  266. let hash = await executeCommand(`git rev-parse HEAD`);
  267. await executeOnSandbox(`
  268. echo '${hash}' > ${sandboxPublicThemesFolder}/.pub-git-hash
  269. `);
  270. }
  271. /*
  272. Version bump (increment version patch) any theme project that has had changes since the last deployment.
  273. If a theme's version has already been changed since that last deployment then do not version bump it.
  274. If any theme projects have had a version bump also version bump the parent project.
  275. Commit the change.
  276. */
  277. async function versionBumpThemes() {
  278. console.log("Version Bumping");
  279. let themes = await getActionableThemes();
  280. let hash = await getLastDeployedHash();
  281. let versionBumpCount = 0;
  282. for (let theme of themes) {
  283. let hasChanges = await checkThemeForChanges(theme, hash);
  284. if( ! hasChanges){
  285. // console.log(`${theme} has no changes`);
  286. continue;
  287. }
  288. versionBumpCount++;
  289. let hasVersionBump = await checkThemeForVersionBump(theme, hash);
  290. if( hasVersionBump ){
  291. continue;
  292. }
  293. await versionBumpTheme(theme);
  294. }
  295. //version bump the root project if there were changes to any of the themes
  296. let rootHasVersionBump = await checkThemeForVersionBump('.', hash);
  297. if ( versionBumpCount > 0 && ! rootHasVersionBump ) {
  298. await executeCommand(`npm version patch --no-git-tag-version`);
  299. }
  300. if (versionBumpCount > 0) {
  301. console.log('commiting version-bump');
  302. await executeCommand(`
  303. git commit -a -m "Version Bump";
  304. `, true);
  305. }
  306. }
  307. function getThemeMetadata(styleCss, attribute) {
  308. if ( !styleCss || !attribute ) {
  309. return null;
  310. }
  311. switch ( attribute ) {
  312. case 'Version':
  313. return styleCss
  314. .match(/(?<=Version:\s*).*?(?=\s*\r?\n|\rg)/gs)[0]
  315. .trim()
  316. .replace('-wpcom', '');
  317. case 'Tested up to':
  318. return styleCss
  319. .match(/(?<=Tested up to:\s*).*?(?=\s*\r?\n|\rg)/gs);
  320. }
  321. }
  322. /*
  323. Version Bump a Theme.
  324. Used by versionBumpThemes to do the work of version bumping.
  325. First increment the patch version in style.css
  326. Then update any of these files with the new version: [package.json, style.scss, style-child-theme.scss]
  327. */
  328. async function versionBumpTheme(theme){
  329. console.log(`${theme} needs a version bump`);
  330. await executeCommand(`perl -pi -e 's/Version: ((\\d+\\.)*)(\\d+)(.*)$/"Version: ".$1.($3+1).$4/ge' ${theme}/style.css`, true);
  331. let styleCss = fs.readFileSync(`${theme}/style.css`, 'utf8');
  332. let currentVersion = getThemeMetadata(styleCss, 'Version');
  333. let filesToUpdate = await executeCommand(`find ${theme} -name package.json -o -name style.scss -o -name style-child-theme.scss -maxdepth 2`);
  334. filesToUpdate = filesToUpdate.split('\n').filter(item => item != '');
  335. for ( let file of filesToUpdate ) {
  336. await executeCommand(`perl -pi -e 's/Version: (.*)$/"Version: '${currentVersion}'"/ge' ${file}`);
  337. await executeCommand(`perl -pi -e 's/\\"version\\": (.*)$/"\\"version\\": \\"'${currentVersion}'\\","/ge' ${file}`);
  338. }
  339. }
  340. /*
  341. Determine if a theme has had a version bump since a given hash.
  342. Used by versionBumpThemes
  343. Compares the value of 'version' in style.css between the hash and current value
  344. */
  345. async function checkThemeForVersionBump(theme, hash){
  346. return executeCommand(`
  347. git show ${hash}:${theme}/style.css 2>/dev/null
  348. `)
  349. .catch( ( error ) => {
  350. //This is a new theme, no need to bump versions so we'll just say we've already done it
  351. return true;
  352. } )
  353. .then( ( previousStyleString ) => {
  354. if( previousStyleString === true) {
  355. return previousStyleString;
  356. }
  357. let previousVersion = getThemeMetadata(previousStyleString, 'Version');
  358. let styleCss = fs.readFileSync(`${theme}/style.css`, 'utf8');
  359. let currentVersion = getThemeMetadata(styleCss, 'Version');
  360. return previousVersion != currentVersion;
  361. });
  362. }
  363. /*
  364. Determine if a theme has had changes since a given hash.
  365. Used by versionBumpThemes
  366. */
  367. async function checkThemeForChanges(theme, hash){
  368. let uncomittedChanges = await executeCommand(`git diff-index --name-only HEAD -- ${theme}`);
  369. let comittedChanges = await executeCommand(`git diff --name-only ${hash} HEAD -- ${theme}`);
  370. return uncomittedChanges != '' || comittedChanges != '';
  371. }
  372. /*
  373. Provide a list of 'actionable' themes (those themes that have style.css files)
  374. */
  375. async function getActionableThemes() {
  376. let result = await executeCommand(`for d in */; do
  377. if test -f "./$d/style.css"; then
  378. echo $d;
  379. fi
  380. done`);
  381. return result
  382. .split('\n')
  383. .map(item=>item.replace('/', ''));
  384. }
  385. /*
  386. Clean the theme sandbox.
  387. Assumes sandbox is in 'git' mode
  388. checkout origin/develop and ensure it's up-to-date.
  389. Remove any other changes.
  390. */
  391. async function cleanSandboxGit() {
  392. console.log('Cleaning the Themes Sandbox');
  393. await executeOnSandbox(`
  394. cd ${sandboxPublicThemesFolder};
  395. git reset --hard HEAD;
  396. git clean -fd;
  397. git checkout develop;
  398. git pull;
  399. echo;
  400. git status
  401. `, true);
  402. console.log('All done cleaning.');
  403. }
  404. /*
  405. Clean the entire sandbox.
  406. Assumes sandbox is in 'git' mode
  407. checkout origin/develop and ensure it's up-to-date.
  408. Remove any other changes.
  409. */
  410. async function cleanAllSandboxGit() {
  411. console.log('Cleaning the Entire Sandbox');
  412. let response = await executeOnSandbox(`
  413. cd ${sandboxRootFolder};
  414. git reset --hard HEAD;
  415. git clean -fd;
  416. git checkout develop;
  417. git pull;
  418. echo;
  419. git status
  420. `, true);
  421. console.log('All done cleaning.');
  422. }
  423. /*
  424. Clean the theme sandbox.
  425. Assumes sandbox is in 'svn' mode
  426. ensure trunk is up-to-date
  427. Remove any other changes
  428. */
  429. async function cleanSandboxSvn() {
  430. console.log('Cleaning the theme sandbox');
  431. await executeOnSandbox(`
  432. cd ${sandboxPublicThemesFolder};
  433. svn revert -R .;
  434. svn cleanup --remove-unversioned;
  435. svn up;
  436. `, true);
  437. console.log('All done cleaning.');
  438. }
  439. /*
  440. Clean the entire sandbox.
  441. Assumes sandbox is in 'svn' mode
  442. ensure trunk is up-to-date
  443. Remove any other changes
  444. */
  445. async function cleanAllSandboxSvn() {
  446. console.log('Cleaning the entire sandbox');
  447. await executeOnSandbox(`
  448. cd ${sandboxRootFolder};
  449. svn revert -R .;
  450. svn cleanup --remove-unversioned;
  451. svn up .;
  452. `, true);
  453. console.log('All done cleaning.');
  454. }
  455. /*
  456. Push exactly what is here (all files) up to the sandbox (with the exclusion of files noted in .sandbox-ignore)
  457. */
  458. function pushToSandbox() {
  459. executeCommand(`
  460. rsync -av --no-p --no-times --exclude-from='.sandbox-ignore' ./ wpcom-sandbox:${sandboxPublicThemesFolder}/
  461. `);
  462. }
  463. /*
  464. Push only (and every) change since the point-of-diversion from /trunk
  465. Remove files from the sandbox that have been removed since the last deployed hash
  466. */
  467. async function pushChangesToSandbox() {
  468. console.log("Pushing Changes to Sandbox.");
  469. let hash = await getLastDeployedHash();
  470. let deletedFiles = await getDeletedFilesSince(hash);
  471. let changedFiles = await getComittedChangesSinceHash(hash);
  472. //remove deleted files from changed files
  473. changedFiles = changedFiles.filter( item => {
  474. return false === deletedFiles.includes(item);
  475. });
  476. if(deletedFiles.length > 0) {
  477. console.log('deleting from sandbox: ', deletedFiles);
  478. await executeOnSandbox(`
  479. cd ${sandboxPublicThemesFolder};
  480. rm -f ${deletedFiles.join(' ')}
  481. `, true);
  482. }
  483. if(changedFiles.length > 0) {
  484. console.log('pushing changed files to sandbox:', changedFiles);
  485. await executeCommand(`
  486. rsync -avR --no-p --no-times --exclude-from='.sandbox-ignore' ${changedFiles.join(' ')} wpcom-sandbox:${sandboxPublicThemesFolder}/
  487. `, true);
  488. }
  489. }
  490. /*
  491. Provide a collection of all files that have changed since the given hash.
  492. Used by pushChangesToSandbox
  493. */
  494. async function getComittedChangesSinceHash(hash) {
  495. let comittedChanges = await executeCommand(`git diff ${hash} HEAD --name-only`);
  496. comittedChanges = comittedChanges.replace(/\r?\n|\r/g, " ").split(" ");
  497. let uncomittedChanges = await executeCommand(`git diff HEAD --name-only`);
  498. uncomittedChanges = uncomittedChanges.replace(/\r?\n|\r/g, " ").split(" ");
  499. return comittedChanges.concat(uncomittedChanges);
  500. }
  501. /*
  502. Provide a collection of all files that have been deleted since the given hash.
  503. Used by pushChangesToSandbox
  504. */
  505. async function getDeletedFilesSince(hash){
  506. let deletedSinceHash = await executeCommand(`
  507. git log --format=format:"" --name-only -M100% --diff-filter=D ${hash}..HEAD
  508. `);
  509. deletedSinceHash = deletedSinceHash.replace(/\r?\n|\r/g, " ").trim().split(" ");
  510. let deletedAndUncomitted = await executeCommand(`
  511. git diff HEAD --name-only --diff-filter=D
  512. `);
  513. deletedAndUncomitted = deletedAndUncomitted.replace(/\r?\n|\r/g, " ").trim().split(" ");
  514. return deletedSinceHash.concat(deletedAndUncomitted).filter( item => {
  515. return item != '';
  516. });
  517. }
  518. /*
  519. Build the Phabricator commit message.
  520. This message contains the logs from all of the commits since the given hash.
  521. Used by create*PhabricatorDiff
  522. */
  523. async function buildPhabricatorCommitMessageSince(hash){
  524. let projectVersion = await executeCommand(`node -p "require('./package.json').version"`);
  525. let logs = await executeCommand(`git log --reverse --pretty=format:%s ${hash}..HEAD`);
  526. return `Deploy Themes ${projectVersion} to wpcom
  527. Summary:
  528. ${logs}
  529. Test Plan: Execute Smoke Test
  530. Reviewers:
  531. Subscribers:
  532. `;
  533. }
  534. /*
  535. Create a (git) Phabricator diff from a given hash.
  536. Open the phabricator diff in your browser.
  537. Provide the URL of the phabricator diff.
  538. */
  539. async function createGitPhabricatorDiff(hash) {
  540. console.log('creating Phabricator Diff');
  541. let commitMessage = await buildPhabricatorCommitMessageSince(hash);
  542. let result = await executeOnSandbox(`
  543. cd ${sandboxPublicThemesFolder};
  544. git branch -D deploy
  545. git checkout -b deploy
  546. git add --all
  547. git commit -m "${commitMessage}"
  548. arc diff --create --verbatim
  549. `, true);
  550. let phabricatorUrl = getPhabricatorUrlFromResponse(result);
  551. console.log('Diff Created at: ', phabricatorUrl);
  552. if(phabricatorUrl) {
  553. open(phabricatorUrl);
  554. }
  555. return phabricatorUrl;
  556. }
  557. /*
  558. Create a (svn) Phabricator diff from a given hash.
  559. Open the phabricator diff in your browser.
  560. Provide the URL of the phabricator diff.
  561. */
  562. async function createSvnPhabricatorDiff(hash) {
  563. console.log('creating Phabricator Diff');
  564. const commitTempFileLocation = '/tmp/theme-deploy-comment.txt';
  565. const commitMessage = await buildPhabricatorCommitMessageSince(hash);
  566. console.log(commitMessage);
  567. const result = await executeOnSandbox(`
  568. cd ${sandboxPublicThemesFolder};
  569. echo "${commitMessage}" > ${commitTempFileLocation};
  570. svn add --force * --auto-props --parents --depth infinity -q;
  571. svn status | grep "^\!" | sed 's/^\! *//g' | xargs svn rm;
  572. arc diff --create --message-file ${commitTempFileLocation}
  573. `, true);
  574. const phabricatorUrl = getPhabricatorUrlFromResponse(result);
  575. console.log('Diff Created at: ', phabricatorUrl);
  576. if(phabricatorUrl) {
  577. open(phabricatorUrl);
  578. }
  579. return phabricatorUrl;
  580. }
  581. /*
  582. Utility to pull the Phabricator URL from the diff creation command.
  583. Used by createGitPhabricatorDiff
  584. */
  585. function getPhabricatorUrlFromResponse(response){
  586. return response
  587. ?.split('\n')
  588. ?.find( item => {
  589. return item.includes('Revision URI: ');
  590. })
  591. ?.split("Revision URI: ")[1];
  592. }
  593. /*
  594. Create a git tag at the current hash.
  595. In the description include the commit logs since the given hash.
  596. Include the (cleansed) Phabricator link.
  597. */
  598. async function tagDeployment(options={}) {
  599. let hash = options.hash || await getLastDeployedHash();
  600. let workInTheOpenPhabricatorUrl = '';
  601. if (options.diffId) {
  602. workInTheOpenPhabricatorUrl = `Phabricator: ${options.diffId}-code`;
  603. }
  604. let projectVersion = await executeCommand(`node -p "require('./package.json').version"`);
  605. let logs = await executeCommand(`git log --reverse --pretty=format:%s ${hash}..HEAD`);
  606. let tag = `v${projectVersion}`;
  607. let message = `Deploy Themes ${tag} to wpcom. \n\n${logs} \n\n${workInTheOpenPhabricatorUrl}`;
  608. await executeCommand(`
  609. git tag -a ${tag} -m "${message}"
  610. git push origin ${tag}
  611. `);
  612. }
  613. /*
  614. Execute a command on the sandbox.
  615. Expects the following to be configured in your ~/.ssh/config file:
  616. Host wpcom-sandbox
  617. User wpdev
  618. HostName SANDBOXURL.wordpress.com
  619. ForwardAgent yes
  620. */
  621. function executeOnSandbox(command, logResponse, enablePsudoterminal){
  622. if(enablePsudoterminal){
  623. return executeCommand(`ssh -tt -A ${remoteSSH} << EOF
  624. ${command}
  625. EOF`, logResponse);
  626. }
  627. return executeCommand(`ssh -TA ${remoteSSH} << EOF
  628. ${command}
  629. EOF`, logResponse);
  630. }
  631. /*
  632. Execute a command locally.
  633. */
  634. async function executeCommand(command, logResponse) {
  635. return new Promise((resolove, reject) => {
  636. let child;
  637. let response = '';
  638. let errResponse = '';
  639. if (isWin) {
  640. child = spawn('cmd.exe', ['/s', '/c', '"' + command + '"'], {
  641. windowsVerbatimArguments: true,
  642. stdio: [process.stdin, 'pipe', 'pipe'],
  643. })
  644. } else {
  645. child = spawn(process.env.SHELL, ['-c', command]);
  646. }
  647. child.stdout.on('data', (data) => {
  648. response += data;
  649. if(logResponse){
  650. console.log(data.toString());
  651. }
  652. });
  653. child.stderr.on('data', (data) => {
  654. errResponse += data;
  655. if(logResponse){
  656. console.log(data.toString());
  657. }
  658. });
  659. child.on('exit', (code) => {
  660. if (code !== 0) {
  661. reject(errResponse.trim());
  662. }
  663. resolove(response.trim());
  664. });
  665. });
  666. }