theme-utils.mjs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911
  1. import { spawn } from 'child_process';
  2. import fs from 'fs';
  3. import open from 'open';
  4. import inquirer from 'inquirer';
  5. const remoteSSH = 'wpcom-sandbox';
  6. const sandboxPublicThemesFolder = '/home/wpdev/public_html/wp-content/themes/pub';
  7. const sandboxPremiumThemesFolder = '/home/wpdev/public_html/wp-content/themes/premium';
  8. const sandboxRootFolder = '/home/wpdev/public_html/';
  9. const isWin = process.platform === 'win32';
  10. const premiumThemes = [ 'videomaker', 'videomaker-white' ];
  11. const coreThemes = ['twentyten', 'twentyeleven', 'twentytwelve', 'twentythirteen', 'twentyfourteen', 'twentyfifteen', 'twentysixteen', 'twentyseventeen', 'twentynineteen', 'twentytwenty', 'twentytwentyone', 'twentytwentytwo'];
  12. (async function start() {
  13. let args = process.argv.slice(2);
  14. let command = args?.[0];
  15. switch (command) {
  16. case "push-button-deploy": return pushButtonDeploy();
  17. case "clean-sandbox": return cleanSandbox();
  18. case "clean-premium-sandbox": return cleanPremiumSandbox();
  19. case "clean-all-sandbox": return cleanAllSandbox();
  20. case "push-to-sandbox": return pushToSandbox();
  21. case "push-changes-to-sandbox": return pushChangesToSandbox();
  22. case "push-premium-to-sandbox": return pushPremiumToSandbox();
  23. case "version-bump-themes": return versionBumpThemes();
  24. case "land-diff": return landChanges(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 "pull-core-themes": return pullCoreThemes();
  29. case "push-core-themes": return pushCoreThemes();
  30. case "sync-core-theme": return syncCoreTheme(args?.[1], args?.[2]);
  31. case "deploy-sync-core-theme": return deploySyncCoreTheme(args?.[1], args?.[2]);
  32. }
  33. return showHelp();
  34. })();
  35. function showHelp(){
  36. // TODO: make this helpful
  37. console.log('Help info can go here');
  38. }
  39. /*
  40. Determine what changes would be deployed
  41. */
  42. async function deployPreview() {
  43. console.clear();
  44. console.log('To ensure accuracy clean your sandbox before previewing. (It is not automatically done).');
  45. let message = await checkForDeployability();
  46. if (message) {
  47. console.log(`\n${message}\n\n`);
  48. }
  49. let hash = await getLastDeployedHash();
  50. console.log(`Last deployed hash: ${hash}`);
  51. let changedThemes = await getChangedThemes(hash);
  52. console.log(`The following themes have changes:\n${changedThemes}`);
  53. let logs = await executeCommand(`git log --reverse --pretty=format:%s ${hash}..HEAD`);
  54. console.log(`\n\nCommit log of changes to be deployed:\n\n${logs}\n\n`);
  55. }
  56. /*
  57. Execute the first phase of a deployment.
  58. * Gets the last deployed hash from the sandbox
  59. * Version bump all themes have have changes since the last deployment
  60. * Commit the version bump change to github
  61. * Clean the sandbox and ensure it is up-to-date
  62. * Push all changed files (including removal of deleted files) since the last deployment
  63. * Update the 'last deployed' hash on the sandbox
  64. * Create a phabricator diff based on the changes since the last deployment. The description including the commit messages since the last deployment.
  65. * Open the Phabricator Diff in your browser
  66. * Create a tag in the github repository at this point of change which includes the phabricator link in the description
  67. */
  68. async function pushButtonDeploy() {
  69. console.clear();
  70. let prompt = await inquirer.prompt([{
  71. type: 'confirm',
  72. message: 'You are about to deploy /trunk. Are you ready to continue?',
  73. name: "continue",
  74. default: false
  75. }]);
  76. if(!prompt.continue){
  77. return;
  78. }
  79. let message = await checkForDeployability();
  80. if (message) {
  81. return console.log(`\n\n${message}\n\n`);
  82. }
  83. try {
  84. await cleanSandbox();
  85. //build variations
  86. console.log('Building Variations');
  87. await executeCommand(`node ./variations/build-variations.mjs git-add-changes`)
  88. prompt = await inquirer.prompt([{
  89. type: 'confirm',
  90. message: 'Are you good with any staged theme variations changes? Make any manual adjustments now if necessary.',
  91. name: "continue",
  92. default: false
  93. }]);
  94. if(!prompt.continue){
  95. console.log(`Aborted Automated Deploy Process at variations building.` );
  96. return;
  97. }
  98. try {
  99. await executeCommand(`
  100. git commit -m "Building Variations"
  101. `);
  102. } catch (err) {
  103. // Most likely the error is that there are no variation changes to commit.
  104. // Just swallowing that error for now
  105. }
  106. let hash = await getLastDeployedHash();
  107. let thingsWentBump = await versionBumpThemes();
  108. if( thingsWentBump ){
  109. prompt = await inquirer.prompt([{
  110. type: 'confirm',
  111. message: 'Are you good with the version bump changes? Make any manual adjustments now if necessary.',
  112. name: "continue",
  113. default: false
  114. }]);
  115. if(!prompt.continue){
  116. console.log(`Aborted Automated Deploy Process at version bump changes.` );
  117. return;
  118. }
  119. }
  120. let changedThemes = await getChangedThemes(hash);
  121. await pushChangesToSandbox();
  122. //push changes (from version bump)
  123. if( thingsWentBump ){
  124. prompt = await inquirer.prompt([{
  125. type: 'confirm',
  126. message: 'Are you ready to push this version bump change to the source repository (Github)?',
  127. name: "continue",
  128. default: false
  129. }]);
  130. if(!prompt.continue){
  131. console.log(`Aborted Automated Deploy Process at version bump push change.` );
  132. return;
  133. }
  134. await executeCommand(`
  135. git commit -m "Version Bump";
  136. git push
  137. `, true);
  138. }
  139. await updateLastDeployedHash();
  140. let commitMessage = await buildPhabricatorCommitMessageSince(hash);
  141. let diffUrl = await createPhabricatorDiff(commitMessage);
  142. let diffId = diffUrl.split('a8c.com/')[1];
  143. await tagDeployment({
  144. hash: hash,
  145. diffId: diffId
  146. });
  147. 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`);
  148. prompt = await inquirer.prompt([{
  149. type: 'confirm',
  150. message: 'Are you ready to land these changes?',
  151. name: "continue",
  152. default: false
  153. }]);
  154. if(!prompt.continue){
  155. 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}` );
  156. return;
  157. }
  158. await landChanges(diffId);
  159. let changedPublicThemes = changedThemes.filter( item=> ! premiumThemes.includes( item ) );
  160. try {
  161. await deployThemes(changedPublicThemes);
  162. }
  163. catch (err) {
  164. prompt = await inquirer.prompt([{
  165. type: 'confirm',
  166. message: `There was an error deploying themes. ${err} Do you wish to continue to the next step?`,
  167. name: "continue",
  168. default: false
  169. }]);
  170. if(!prompt.continue){
  171. console.log(`Aborted Automated Deploy during deploy phase.` );
  172. return;
  173. }
  174. }
  175. await buildComZips(changedPublicThemes);
  176. console.log(`The following themes have changed:\n${changedThemes.join('\n')}`)
  177. console.log('\n\nAll Done!!\n\n');
  178. }
  179. catch (err) {
  180. console.log("ERROR with deploy script: ", err);
  181. }
  182. }
  183. async function deploySyncCoreTheme(theme, sinceRevision) {
  184. await cleanSandbox();
  185. let latestRevision = await syncCoreTheme(theme, sinceRevision);
  186. let prompt = await inquirer.prompt([{
  187. type: 'confirm',
  188. message: `Changes have been synced to your sandbox. Please resolve any conflicts (noted in .rej files). Are you ready to continue?`,
  189. name: "continue",
  190. default: false
  191. }]);
  192. if(!prompt.continue){
  193. console.log(`Aborted Core Sync Deploy.` );
  194. return;
  195. }
  196. let logs = await executeCommand(`svn log https://core.svn.wordpress.org/trunk/wp-content/themes/${theme} -r${sinceRevision}:HEAD`)
  197. let commitMessage = `${theme}: Merge latest core changes up to [wp${latestRevision}]
  198. Summary:
  199. ${logs}
  200. Test Plan: Activate ${theme} and ensure nothing is broken
  201. Reviewers:
  202. #themes_team
  203. Subscribers:
  204. `;
  205. let diffUrl = await createPhabricatorDiff (commitMessage);
  206. let diffId = diffUrl.split('a8c.com/')[1];
  207. prompt = await inquirer.prompt([{
  208. type: 'confirm',
  209. message: 'Are you ready to land these changes?',
  210. name: "continue",
  211. default: false
  212. }]);
  213. if(!prompt.continue){
  214. console.log(`Aborted Automated Deploy Sync Process Landing Phase\n\nYou will have to land these changes manually. The ID of the diff to land: ${diffId}` );
  215. return;
  216. }
  217. return;
  218. // await landChanges(diffId);
  219. // await deployThemes([theme]);
  220. // await buildComZips([theme]);
  221. }
  222. /*
  223. Build .zip file for .com
  224. */
  225. async function buildComZip(themeSlug) {
  226. console.log( `Building ${themeSlug} .zip` );
  227. let styleCss = fs.readFileSync(`${themeSlug}/style.css`, 'utf8');
  228. // Gets the theme version (Version:) and minimum WP version (Tested up to:) from the theme's style.css
  229. let themeVersion = getThemeMetadata(styleCss, 'Version');
  230. let wpVersionCompat = getThemeMetadata(styleCss, 'Requires at least');
  231. if (themeVersion && wpVersionCompat) {
  232. await executeOnSandbox(`php ${sandboxRootFolder}bin/themes/theme-downloads/build-theme-zip.php --stylesheet=pub/${themeSlug} --themeversion=${themeVersion} --wpversioncompat=${wpVersionCompat};`, true);
  233. }
  234. else {
  235. console.log('Unable to build theme .zip.');
  236. if (!themeVersion) {
  237. console.log('Could not find theme version (Version:) in the theme style.css.');
  238. }
  239. if (!wpVersionCompat) {
  240. console.log('Could not find WP compat version (Tested up to:) in the theme style.css.');
  241. }
  242. console.log('Please build the .zip file for the theme manually.', themeSlug);
  243. open('https://mc.a8c.com/themes/downloads/');
  244. }
  245. }
  246. async function buildComZips(themes) {
  247. for ( let theme of themes ) {
  248. try {
  249. await buildComZip(theme);
  250. } catch (err) {
  251. console.log(`There was an error building dotcom zip for ${theme}. ${err}`);
  252. }
  253. }
  254. }
  255. /*
  256. Check to ensure that:
  257. * The current branch is /trunk
  258. * That trunk is up-to-date with origin/trunk
  259. */
  260. async function checkForDeployability(){
  261. let branchName = await executeCommand('git symbolic-ref --short HEAD');
  262. if(branchName !== 'trunk' ) {
  263. return 'Only the /trunk branch can be deployed.';
  264. }
  265. await executeCommand('git remote update', true);
  266. let localMasterHash = await executeCommand('git rev-parse trunk')
  267. let remoteMasterHash = await executeCommand('git rev-parse origin/trunk')
  268. if(localMasterHash !== remoteMasterHash) {
  269. return 'Local /trunk is out-of-date. Pull changes to continue.'
  270. }
  271. return null;
  272. }
  273. /*
  274. Land the changes from the given diff ID. This is the "production merge".
  275. */
  276. async function landChanges(diffId){
  277. return executeCommand(`ssh -tt -A ${remoteSSH} "cd ${sandboxPublicThemesFolder}; /usr/local/bin/arc patch ${diffId}; /usr/local/bin/arc land; exit;"`, true);
  278. }
  279. async function getChangedThemes(hash) {
  280. console.log('Determining all changed themes');
  281. let themes = await getActionableThemes();
  282. let changedThemes = [];
  283. for (let theme of themes) {
  284. let hasChanges = await checkThemeForChanges(theme, hash);
  285. if(hasChanges){
  286. changedThemes.push(theme);
  287. }
  288. }
  289. return changedThemes;
  290. }
  291. /*
  292. Deploy a collection of themes.
  293. Part of the push-button-deploy process.
  294. Can also be triggered to deploy a single theme with the command:
  295. node ./theme-utils.mjs deploy-theme THEMENAME
  296. */
  297. async function deployThemes( themes ) {
  298. let response;
  299. for ( let theme of themes ) {
  300. console.log( `Deploying ${theme}` );
  301. let deploySuccess = false;
  302. let attempt = 0;
  303. while ( ! deploySuccess && attempt <= 2 ) {
  304. attempt++;
  305. console.log(`\nattempt #${attempt}\n\n`);
  306. response = await executeOnSandbox( `deploy pub ${theme};exit;`, true, true );
  307. deploySuccess = response.includes( 'successfully deployed to' );
  308. if( ! deploySuccess ) {
  309. console.log( 'Deploy was not successful. Trying again in 10 seconds...' );
  310. await new Promise(resolve => setTimeout(resolve, 10000));
  311. }
  312. else {
  313. console.log( "Deploy successful." );
  314. }
  315. }
  316. if ( ! deploySuccess ) {
  317. await inquirer.prompt([{
  318. type: 'confirm',
  319. message: `${theme} was not sucessfully deployed and should be deployed manually.`,
  320. name: "continue",
  321. default: false
  322. }]);
  323. }
  324. }
  325. }
  326. /*
  327. Provide the hash of the last managed deployment.
  328. This hash is used to determine all the changes that have happened between that point and the current point.
  329. */
  330. async function getLastDeployedHash() {
  331. let result = await executeOnSandbox(`
  332. cat ${sandboxPublicThemesFolder}/.pub-git-hash
  333. `);
  334. return result;
  335. }
  336. /*
  337. Update the 'last deployed hash' on the server with the current hash.
  338. */
  339. async function updateLastDeployedHash() {
  340. let hash = await executeCommand(`git rev-parse HEAD`);
  341. await executeOnSandbox(`
  342. echo '${hash}' > ${sandboxPublicThemesFolder}/.pub-git-hash
  343. `);
  344. }
  345. /*
  346. Version bump (increment version patch) any theme project that has had changes since the last deployment.
  347. If a theme's version has already been changed since that last deployment then do not version bump it.
  348. If any theme projects have had a version bump also version bump the parent project.
  349. Commit the change.
  350. */
  351. async function versionBumpThemes() {
  352. console.log("Version Bumping");
  353. let themes = await getActionableThemes();
  354. let hash = await getLastDeployedHash();
  355. let changesWereMade = false;
  356. let versionBumpCount = 0;
  357. for (let theme of themes) {
  358. let hasChanges = await checkThemeForChanges(theme, hash);
  359. if( ! hasChanges){
  360. // console.log(`${theme} has no changes`);
  361. continue;
  362. }
  363. versionBumpCount++;
  364. let hasVersionBump = await checkThemeForVersionBump(theme, hash);
  365. if( hasVersionBump ){
  366. continue;
  367. }
  368. await versionBumpTheme(theme, true);
  369. changesWereMade = true;
  370. }
  371. //version bump the root project if there were changes to any of the themes
  372. let rootHasVersionBump = await checkProjectForVersionBump(hash);
  373. if ( versionBumpCount > 0 && ! rootHasVersionBump ) {
  374. await executeCommand(`npm version patch --no-git-tag-version && git add package.json package-lock.json`);
  375. changesWereMade = true;
  376. }
  377. return changesWereMade;
  378. }
  379. export function getThemeMetadata(styleCss, attribute) {
  380. if ( !styleCss || !attribute ) {
  381. return null;
  382. }
  383. switch ( attribute ) {
  384. case 'Version':
  385. return styleCss
  386. .match(/(?<=Version:\s*).*?(?=\s*\r?\n|\rg)/gs)[0]
  387. .trim()
  388. .replace('-wpcom', '');
  389. case 'Requires at least':
  390. return styleCss
  391. .match(/(?<=Requires at least:\s*).*?(?=\s*\r?\n|\rg)/gs);
  392. }
  393. }
  394. /*
  395. Version Bump a Theme.
  396. Used by versionBumpThemes to do the work of version bumping.
  397. First increment the patch version in style.css
  398. Then update any of these files with the new version: [package.json, style.scss, style-child-theme.scss]
  399. */
  400. async function versionBumpTheme(theme, addChanges){
  401. console.log(`${theme} needs a version bump`);
  402. await executeCommand(`perl -pi -e 's/Version: ((\\d+\\.)*)(\\d+)(.*)$/"Version: ".$1.($3+1).$4/ge' ${theme}/style.css`, true);
  403. await executeCommand(`git add ${theme}/style.css`);
  404. let styleCss = fs.readFileSync(`${theme}/style.css`, 'utf8');
  405. let currentVersion = getThemeMetadata(styleCss, 'Version');
  406. let filesToUpdate = await executeCommand(`find ${theme} -name package.json -o -name style.scss -o -name style-child-theme.scss -maxdepth 2`);
  407. filesToUpdate = filesToUpdate.split('\n').filter(item => item != '');
  408. for ( let file of filesToUpdate ) {
  409. await executeCommand(`perl -pi -e 's/Version: (.*)$/"Version: '${currentVersion}'"/ge' ${file}`);
  410. await executeCommand(`perl -pi -e 's/\\"version\\": (.*)$/"\\"version\\": \\"'${currentVersion}'\\","/ge' ${file}`);
  411. if (addChanges){
  412. await executeCommand(`git add ${file}`);
  413. }
  414. }
  415. }
  416. /*
  417. Determine if a theme has had a version bump since a given hash.
  418. Used by versionBumpThemes
  419. Compares the value of 'version' in style.css between the hash and current value
  420. */
  421. async function checkThemeForVersionBump(theme, hash){
  422. return executeCommand(`
  423. git show ${hash}:${theme}/style.css 2>/dev/null
  424. `)
  425. .catch( ( error ) => {
  426. //This is a new theme, no need to bump versions so we'll just say we've already done it
  427. return true;
  428. } )
  429. .then( ( previousStyleString ) => {
  430. if( previousStyleString === true) {
  431. return previousStyleString;
  432. }
  433. let previousVersion = getThemeMetadata(previousStyleString, 'Version');
  434. let styleCss = fs.readFileSync(`${theme}/style.css`, 'utf8');
  435. let currentVersion = getThemeMetadata(styleCss, 'Version');
  436. return previousVersion != currentVersion;
  437. });
  438. }
  439. /*
  440. Determine if the project has had a version bump since a given hash.
  441. Used by versionBumpThemes
  442. Compares the value of 'version' in package.json between the hash and current value
  443. */
  444. async function checkProjectForVersionBump(hash){
  445. let previousPackageString = await executeCommand(`
  446. git show ${hash}:./package.json 2>/dev/null
  447. `);
  448. let previousPackage = JSON.parse(previousPackageString);
  449. let currentPackage = JSON.parse(fs.readFileSync(`./package.json`))
  450. return previousPackage.version != currentPackage.version;
  451. }
  452. /*
  453. Determine if a theme has had changes since a given hash.
  454. Used by versionBumpThemes
  455. */
  456. async function checkThemeForChanges(theme, hash){
  457. let comittedChanges = await executeCommand(`git diff --name-only ${hash} HEAD -- ${theme}`);
  458. return comittedChanges != '';
  459. }
  460. /*
  461. Provide a list of 'actionable' themes (those themes that have style.css files)
  462. */
  463. async function getActionableThemes() {
  464. let result = await executeCommand(`for d in */; do
  465. if test -f "./$d/style.css"; then
  466. echo $d;
  467. fi
  468. done`);
  469. return result
  470. .split('\n')
  471. .map(item=>item.replace('/', ''));
  472. }
  473. /*
  474. Clean the theme sandbox.
  475. checkout origin/trunk and ensure it's up-to-date.
  476. Remove any other changes.
  477. */
  478. async function cleanSandbox() {
  479. console.log('Cleaning the Themes Sandbox');
  480. await executeOnSandbox(`
  481. cd ${sandboxPublicThemesFolder};
  482. git reset --hard HEAD;
  483. git clean -fd;
  484. git checkout trunk;
  485. git pull;
  486. echo;
  487. git status
  488. `, true);
  489. console.log('All done cleaning.');
  490. }
  491. /*
  492. Clean the premium theme sandbox.
  493. checkout origin/trunk and ensure it's up-to-date.
  494. Remove any other changes.
  495. */
  496. async function cleanPremiumSandbox() {
  497. console.log('Cleaning the Themes Sandbox');
  498. await executeOnSandbox(`
  499. cd ${sandboxPremiumThemesFolder};
  500. git reset --hard HEAD;
  501. git clean -fd;
  502. git checkout trunk;
  503. git pull;
  504. echo;
  505. git status
  506. `, true);
  507. console.log('All done cleaning.');
  508. }
  509. /*
  510. Clean the entire sandbox.
  511. checkout origin/trunk and ensure it's up-to-date.
  512. Remove any other changes.
  513. */
  514. async function cleanAllSandbox() {
  515. console.log('Cleaning the Entire Sandbox');
  516. let response = await executeOnSandbox(`
  517. cd ${sandboxRootFolder};
  518. git reset --hard HEAD;
  519. git clean -fd;
  520. git checkout trunk;
  521. git pull;
  522. echo;
  523. git status
  524. `, true);
  525. console.log('All done cleaning.');
  526. }
  527. /*
  528. Push exactly what is here (all files) up to the sandbox (with the exclusion of files noted in .sandbox-ignore)
  529. */
  530. async function pushToSandbox() {
  531. console.log("Pushing All Themes to Sandbox.");
  532. let allThemes = await getActionableThemes();
  533. allThemes = allThemes.filter( item=> ! premiumThemes.includes( item ) );
  534. console.log(`Syncing ${allThemes.length} themes`);
  535. for ( let theme of allThemes ) {
  536. await pushThemeToSandbox(theme);
  537. }
  538. }
  539. async function pushThemeToSandbox(theme) {
  540. console.log( `Syncing ${theme}` );
  541. return executeCommand(`
  542. rsync -avR --no-p --no-times --delete -m --exclude-from='.sandbox-ignore' ./${theme}/ wpcom-sandbox:${sandboxPublicThemesFolder}/
  543. `, true);
  544. }
  545. /*
  546. Push exactly what is here (all files) up to the sandbox (with the exclusion of files noted in .sandbox-ignore)
  547. This pushes only the folders noted as "premiumThemes" into the premium themes directory.
  548. This is the only part of the deploy process that is automated; the rest must be done manually including:
  549. * Creating a Phabricator Diff
  550. * Landing (comitting) the change
  551. * Deploying the theme
  552. * Triggering the .zip builds
  553. */
  554. async function pushPremiumToSandbox() {
  555. //TODO: It would be nice to determine this list programatically
  556. const filesToModify = [
  557. 'style.css',
  558. 'block-templates/404.html',
  559. 'block-template-parts/header.html',
  560. 'block-template-parts/footer.html'
  561. ];
  562. // Change 'blockbase' to 'blockbase-premium' in the files noted
  563. for ( let theme of premiumThemes ) {
  564. for ( let file of filesToModify ) {
  565. await executeCommand(`perl -pi -e 's/blockbase/blockbase-premium/' ${theme}/${file}`, true);
  566. }
  567. }
  568. // Push the changes in the premium themes to the sandbox
  569. await executeCommand(`
  570. rsync -avR --no-p --no-times --delete -m --exclude-from='.sandbox-ignore' --exclude='sass' ./${premiumThemes.join(' ./')} wpcom-sandbox:${sandboxPremiumThemesFolder}/
  571. `, true);
  572. // revert the local blockbase-premium changes
  573. for ( let theme of premiumThemes ) {
  574. for ( let file of filesToModify ) {
  575. await executeCommand(`
  576. git restore --source=HEAD --staged --worktree ./${theme}/${file}
  577. `);
  578. }
  579. }
  580. }
  581. /*
  582. Push only (and every) change since the point-of-diversion from /trunk
  583. Remove files from the sandbox that have been removed since the last deployed hash
  584. */
  585. async function pushChangesToSandbox() {
  586. console.log("Pushing Changed Themes to Sandbox.");
  587. let hash = await getLastDeployedHash();
  588. let changedThemes = await getChangedThemes(hash);
  589. changedThemes = changedThemes.filter( item=> ! premiumThemes.includes( item ) );
  590. console.log(`Syncing ${changedThemes.length} themes`);
  591. for ( let theme of changedThemes ) {
  592. await pushThemeToSandbox(theme);
  593. }
  594. }
  595. async function pullCoreThemes() {
  596. console.log("Pulling CORE themes from sandbox.");
  597. for (let theme of coreThemes ) {
  598. await executeCommand(`
  599. rsync -avr --no-p --no-times --delete -m --exclude-from='.sandbox-ignore' wpcom-sandbox:${sandboxPublicThemesFolder}/${theme}/ ./${theme}/
  600. `, true);
  601. }
  602. }
  603. async function pushCoreThemes() {
  604. console.log("Pushing CORE themes to sandbox.");
  605. for (let theme of coreThemes ) {
  606. await executeCommand(`
  607. rsync -avr --no-p --no-times --delete -m --exclude-from='.sandbox-ignore' ./${theme}/ wpcom-sandbox:${sandboxPublicThemesFolder}/${theme}/
  608. `, true);
  609. }
  610. }
  611. async function syncCoreTheme(theme, sinceRevision) {
  612. if(!theme){
  613. console.log('Must supply theme to sync and revision to start from');
  614. return;
  615. }
  616. if(!sinceRevision) {
  617. sinceRevision = await executeOnSandbox(`cat ${sandboxPublicThemesFolder}/${theme}/.pub-svn-revision`);
  618. }
  619. let latestRevision = await executeCommand(`svn info -r HEAD https://core.svn.wordpress.org/trunk | grep Revision | egrep -o "[0-9]+"`);
  620. console.log(`syncing core theme ${theme} from ${sinceRevision} to ${latestRevision} on your sandbox`);
  621. try {
  622. await executeOnSandbox(`
  623. cd ${sandboxPublicThemesFolder};
  624. /usr/bin/svn diff --git -r ${sinceRevision}:HEAD https://core.svn.wordpress.org/trunk/wp-content/themes/${theme} | git apply --reject --ignore-space-change --ignore-whitespace -p4 --directory=${theme} -
  625. `, true);
  626. }
  627. catch (err) {
  628. console.log('Error merging:', err);
  629. }
  630. await executeOnSandbox(`
  631. echo '${latestRevision}' > ${sandboxPublicThemesFolder}/${theme}/.pub-svn-revision
  632. `);
  633. return latestRevision;
  634. }
  635. /*
  636. Build the Phabricator commit message.
  637. This message contains the logs from all of the commits since the given hash.
  638. Used by create*PhabricatorDiff
  639. */
  640. async function buildPhabricatorCommitMessageSince(hash){
  641. let projectVersion = await executeCommand(`node -p "require('./package.json').version"`);
  642. let logs = await executeCommand(`git log --reverse --pretty=format:%s ${hash}..HEAD`);
  643. // Remove any double quotes from commit messages
  644. logs.replace(/"/g, '');
  645. return `Deploy Themes ${projectVersion} to wpcom
  646. Summary:
  647. ${logs}
  648. Test Plan: Execute Smoke Test
  649. Reviewers:
  650. Subscribers:
  651. `;
  652. }
  653. /*
  654. Create a Phabricator diff with the given message based on the contents currently in the sandbox.
  655. Open the phabricator diff in your browser.
  656. Provide the URL of the phabricator diff.
  657. */
  658. async function createPhabricatorDiff(commitMessage) {
  659. console.log('creating Phabricator Diff');
  660. let result = await executeOnSandbox(`
  661. cd ${sandboxPublicThemesFolder};
  662. git branch -D deploy
  663. git checkout -b deploy
  664. git add --all
  665. git commit -m "${commitMessage}"
  666. arc diff --create --verbatim
  667. `, true);
  668. let phabricatorUrl = getPhabricatorUrlFromResponse(result);
  669. console.log('Diff Created at: ', phabricatorUrl);
  670. if(phabricatorUrl) {
  671. open(phabricatorUrl);
  672. }
  673. return phabricatorUrl;
  674. }
  675. /*
  676. Utility to pull the Phabricator URL from the diff creation command.
  677. Used by createPhabricatorDiff
  678. */
  679. function getPhabricatorUrlFromResponse(response){
  680. return response
  681. ?.split('\n')
  682. ?.find( item => {
  683. return item.includes('Revision URI: ');
  684. })
  685. ?.split("Revision URI: ")[1];
  686. }
  687. /*
  688. Create a git tag at the current hash.
  689. In the description include the commit logs since the given hash.
  690. Include the (cleansed) Phabricator link.
  691. */
  692. async function tagDeployment(options={}) {
  693. console.log('tagging deployment');
  694. let hash = options.hash || await getLastDeployedHash();
  695. let workInTheOpenPhabricatorUrl = '';
  696. if (options.diffId) {
  697. workInTheOpenPhabricatorUrl = `Phabricator: ${options.diffId}-code`;
  698. }
  699. let projectVersion = await executeCommand(`node -p "require('./package.json').version"`);
  700. let logs = await executeCommand(`git log --reverse --pretty=format:%s ${hash}..HEAD`);
  701. // Remove any double quotes from commit messages
  702. logs.replace(/"/g, '');
  703. let tag = `v${projectVersion}`;
  704. let message = `Deploy Themes ${tag} to wpcom. \n\n${logs} \n\n${workInTheOpenPhabricatorUrl}`;
  705. await executeCommand(`
  706. git tag -a ${tag} -m "${message}"
  707. git push origin ${tag}
  708. `, true);
  709. }
  710. /*
  711. Execute a command on the sandbox.
  712. Expects the following to be configured in your ~/.ssh/config file:
  713. Host wpcom-sandbox
  714. User wpdev
  715. HostName SANDBOXURL.wordpress.com
  716. ForwardAgent yes
  717. */
  718. function executeOnSandbox(command, logResponse, enablePsudoterminal){
  719. if(enablePsudoterminal){
  720. return executeCommand(`ssh -tt -A ${remoteSSH} << EOF
  721. ${command}
  722. EOF`, logResponse);
  723. }
  724. return executeCommand(`ssh -TA ${remoteSSH} << EOF
  725. ${command}
  726. EOF`, logResponse);
  727. }
  728. /*
  729. Execute a command locally.
  730. */
  731. export async function executeCommand(command, logResponse) {
  732. return new Promise((resolove, reject) => {
  733. let child;
  734. let response = '';
  735. let errResponse = '';
  736. if (isWin) {
  737. child = spawn('cmd.exe', ['/s', '/c', '"' + command + '"'], {
  738. windowsVerbatimArguments: true,
  739. stdio: [process.stdin, 'pipe', 'pipe'],
  740. })
  741. } else {
  742. child = spawn(process.env.SHELL, ['-c', command], {
  743. stdio: [process.stdin, 'pipe', 'pipe'],
  744. });
  745. }
  746. child.stdout.on('data', (data) => {
  747. response += data;
  748. if(logResponse){
  749. console.log(data.toString());
  750. }
  751. });
  752. child.stderr.on('data', (data) => {
  753. errResponse += data;
  754. if(logResponse){
  755. console.log(data.toString());
  756. }
  757. });
  758. child.on('exit', (code) => {
  759. if (code !== 0) {
  760. reject(errResponse.trim());
  761. }
  762. resolove(response.trim());
  763. });
  764. });
  765. }