continuous-integration-script.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. #!/usr/bin/env node
  2. /**
  3. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  4. * For licensing, see LICENSE.md.
  5. */
  6. /* eslint-env node */
  7. 'use strict';
  8. const childProcess = require( 'child_process' );
  9. const crypto = require( 'crypto' );
  10. const fs = require( 'fs' );
  11. const path = require( 'path' );
  12. const glob = require( 'glob' );
  13. const failedChecks = {
  14. dependency: new Set(),
  15. unitTests: new Set(),
  16. codeCoverage: new Set()
  17. };
  18. const RED = '\x1B[0;31m';
  19. const YELLOW = '\x1B[33;1m';
  20. const NO_COLOR = '\x1B[0m';
  21. const travis = {
  22. _lastTimerId: null,
  23. _lastStartTime: null,
  24. foldStart( packageName, foldLabel ) {
  25. console.log( `travis_fold:start:${ packageName }${ YELLOW }${ foldLabel }${ NO_COLOR }` );
  26. this._timeStart();
  27. },
  28. foldEnd( packageName ) {
  29. this._timeFinish();
  30. console.log( `\ntravis_fold:end:${ packageName }\n` );
  31. },
  32. _timeStart() {
  33. const nanoSeconds = process.hrtime.bigint();
  34. this._lastTimerId = crypto.createHash( 'md5' ).update( nanoSeconds.toString() ).digest( 'hex' );
  35. this._lastStartTime = nanoSeconds;
  36. // Intentional direct write to stdout, to manually control EOL.
  37. process.stdout.write( `travis_time:start:${ this._lastTimerId }\r\n` );
  38. },
  39. _timeFinish() {
  40. const travisEndTime = process.hrtime.bigint();
  41. const duration = travisEndTime - this._lastStartTime;
  42. // Intentional direct write to stdout, to manually control EOL.
  43. process.stdout.write( `\ntravis_time:end:${ this._lastTimerId }:start=${ this._lastStartTime },` +
  44. `finish=${ travisEndTime },duration=${ duration }\r\n` );
  45. }
  46. };
  47. childProcess.execSync( 'rm -r -f .nyc_output' );
  48. childProcess.execSync( 'mkdir .nyc_output' );
  49. childProcess.execSync( 'rm -r -f .out' );
  50. childProcess.execSync( 'mkdir .out' );
  51. const packages = childProcess.execSync( 'ls packages -1', {
  52. encoding: 'utf8'
  53. } ).toString().trim().split( '\n' );
  54. for ( const fullPackageName of packages ) {
  55. const simplePackageName = fullPackageName.replace( /^ckeditor5?-/, '' );
  56. const foldLabelName = 'pkg-' + simplePackageName;
  57. travis.foldStart( foldLabelName, `Testing ${ fullPackageName }${ NO_COLOR }` );
  58. appendCoverageReport();
  59. runSubprocess( 'npx', [ 'ckeditor5-dev-tests-check-dependencies', `packages/${ fullPackageName }` ], simplePackageName, 'dependency',
  60. 'have a dependency problem' );
  61. const testArguments = [ 'run', 'test', '-f', simplePackageName, '--reporter=dots', '--production', '--coverage' ];
  62. runSubprocess( 'yarn', testArguments, simplePackageName, 'unitTests', 'failed to pass unit tests' );
  63. childProcess.execSync( 'cp coverage/*/coverage-final.json .nyc_output' );
  64. const nyc = [ 'nyc', 'check-coverage', '--branches', '100', '--functions', '100', '--lines', '100', '--statements', '100' ];
  65. runSubprocess( 'npx', nyc, simplePackageName, 'codeCoverage', 'doesn\'t have required code coverage' );
  66. travis.foldEnd( foldLabelName );
  67. }
  68. console.log( 'Uploading combined code coverage report…' );
  69. if ( shouldUploadCoverageReport() ) {
  70. childProcess.execSync( 'npx coveralls < .out/combined_lcov.info' );
  71. } else {
  72. console.log( 'Since the PR comes from the community, we do not upload code coverage report.' );
  73. console.log( 'Read more why: https://github.com/ckeditor/ckeditor5/issues/7745.' );
  74. }
  75. console.log( 'Done' );
  76. if ( Object.values( failedChecks ).some( checksSet => checksSet.size > 0 ) ) {
  77. console.log( '\n---\n' );
  78. showFailedCheck( 'dependency', 'The following packages have dependencies that are not included in its package.json' );
  79. showFailedCheck( 'unitTests', 'The following packages did not pass unit tests' );
  80. showFailedCheck( 'codeCoverage', 'The following packages did not provide required code coverage' );
  81. process.exit( 1 ); // Exit code 1 will break the CI build.
  82. }
  83. /*
  84. * @param {String} binaryName - Name of a CLI binary to be called.
  85. * @param {String[]} cliArguments - An array of arguments to be passed to the `binaryName`.
  86. * @param {String} packageName - Checked package name.
  87. * @param {String} checkName - A key associated with the problem in the `failedChecks` dictionary.
  88. * @param {String} failMessage - Message to be shown if check failed.
  89. */
  90. function runSubprocess( binaryName, cliArguments, packageName, checkName, failMessage ) {
  91. const subprocess = childProcess.spawnSync( binaryName, cliArguments, {
  92. encoding: 'utf8',
  93. shell: true
  94. } );
  95. console.log( subprocess.stdout );
  96. if ( subprocess.stderr ) {
  97. console.log( subprocess.stderr );
  98. }
  99. if ( subprocess.status !== 0 ) {
  100. failedChecks.unitTests.add( packageName );
  101. console.log( `💥 ${ RED }${ packageName }${ NO_COLOR } ` + failMessage + ' 💥' );
  102. }
  103. }
  104. function showFailedCheck( checkKey, errorMessage ) {
  105. const failedPackages = failedChecks[ checkKey ];
  106. if ( failedPackages.size ) {
  107. console.log( `${ errorMessage }: ${ RED }${ Array.from( failedPackages.values() ).join( ', ' ) }${ NO_COLOR }` );
  108. }
  109. }
  110. function appendCoverageReport() {
  111. // Appends coverage data to the combined code coverage info file. It's used because all the results
  112. // needs to be uploaded at once (#6742).
  113. const matches = glob.sync( 'coverage/*/lcov.info' );
  114. matches.forEach( filePath => {
  115. const buffer = fs.readFileSync( filePath );
  116. fs.writeFileSync( [ '.out', 'combined_lcov.info' ].join( path.sep ), buffer, {
  117. flag: 'as'
  118. } );
  119. } );
  120. }
  121. function shouldUploadCoverageReport() {
  122. // If the repository slugs are different, the pull request comes from the community (forked repository).
  123. // For such builds, sending the CC report will be disabled.
  124. return ( process.env.TRAVIS_EVENT_TYPE !== 'pull_request' || process.env.TRAVIS_PULL_REQUEST_SLUG === process.env.TRAVIS_REPO_SLUG );
  125. }