ci-release-build.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. if (!process.env.CI) require('dotenv-safe').load();
  2. const assert = require('assert');
  3. const got = require('got');
  4. const { Octokit } = require('@octokit/rest');
  5. const BUILD_APPVEYOR_URL = 'https://ci.appveyor.com/api/builds';
  6. const CIRCLECI_PIPELINE_URL = 'https://circleci.com/api/v2/project/gh/electron/electron/pipeline';
  7. const CIRCLECI_WAIT_TIME = process.env.CIRCLECI_WAIT_TIME || 30000;
  8. const appVeyorJobs = {
  9. 'electron-x64': 'electron-x64-release',
  10. 'electron-ia32': 'electron-ia32-release',
  11. 'electron-woa': 'electron-woa-release'
  12. };
  13. const circleCIPublishWorkflows = [
  14. 'linux-publish',
  15. 'macos-publish'
  16. ];
  17. const circleCIPublishIndividualArches = {
  18. 'macos-publish': ['osx-x64', 'mas-x64', 'osx-arm64', 'mas-arm64'],
  19. 'linux-publish': ['arm', 'arm64', 'x64']
  20. };
  21. let jobRequestedCount = 0;
  22. async function makeRequest ({ auth, url, headers, body, method }) {
  23. const clonedHeaders = {
  24. ...(headers || {})
  25. };
  26. if (auth && auth.bearer) {
  27. clonedHeaders.Authorization = `Bearer ${auth.bearer}`;
  28. }
  29. const response = await got(url, {
  30. headers: clonedHeaders,
  31. body,
  32. method,
  33. auth: auth && (auth.username || auth.password) ? `${auth.username}:${auth.password}` : undefined
  34. });
  35. if (response.statusCode < 200 || response.statusCode >= 300) {
  36. console.error('Error: ', `(status ${response.statusCode})`, response.body);
  37. throw new Error(`Unexpected status code ${response.statusCode} from ${url}`);
  38. }
  39. return JSON.parse(response.body);
  40. }
  41. async function circleCIcall (targetBranch, workflowName, options) {
  42. console.log(`Triggering CircleCI to run build job: ${workflowName} on branch: ${targetBranch} with release flag.`);
  43. const buildRequest = {
  44. branch: targetBranch,
  45. parameters: {}
  46. };
  47. if (options.ghRelease) {
  48. buildRequest.parameters['upload-to-storage'] = '0';
  49. } else {
  50. buildRequest.parameters['upload-to-storage'] = '1';
  51. }
  52. buildRequest.parameters[`run-${workflowName}`] = true;
  53. if (options.arch) {
  54. const validArches = circleCIPublishIndividualArches[workflowName];
  55. assert(validArches.includes(options.arch), `Unknown CircleCI architecture "${options.arch}". Valid values are ${JSON.stringify(validArches)}`);
  56. buildRequest.parameters['macos-publish-arch-limit'] = options.arch;
  57. }
  58. jobRequestedCount++;
  59. // The logic below expects that the CircleCI workflows for releases each
  60. // contain only one job in order to maintain compatibility with sudowoodo.
  61. // If the workflows are changed in the CircleCI config.yml, this logic will
  62. // also need to be changed as well as possibly changing sudowoodo.
  63. try {
  64. const circleResponse = await circleCIRequest(CIRCLECI_PIPELINE_URL, 'POST', buildRequest);
  65. console.log(`CircleCI release build pipeline ${circleResponse.id} for ${workflowName} triggered.`);
  66. const workflowId = await getCircleCIWorkflowId(circleResponse.id);
  67. if (workflowId === -1) {
  68. return;
  69. }
  70. const workFlowUrl = `https://circleci.com/workflow-run/${workflowId}`;
  71. if (options.runningPublishWorkflows) {
  72. console.log(`CircleCI release workflow request for ${workflowName} successful. Check ${workFlowUrl} for status.`);
  73. } else {
  74. console.log(`CircleCI release build workflow running at https://circleci.com/workflow-run/${workflowId} for ${workflowName}.`);
  75. const jobNumber = await getCircleCIJobNumber(workflowId);
  76. if (jobNumber === -1) {
  77. return;
  78. }
  79. const jobUrl = `https://circleci.com/gh/electron/electron/${jobNumber}`;
  80. console.log(`CircleCI release build request for ${workflowName} successful. Check ${jobUrl} for status.`);
  81. }
  82. } catch (err) {
  83. console.log('Error calling CircleCI: ', err);
  84. }
  85. }
  86. async function getCircleCIWorkflowId (pipelineId) {
  87. const pipelineInfoUrl = `https://circleci.com/api/v2/pipeline/${pipelineId}`;
  88. let workflowId = 0;
  89. while (workflowId === 0) {
  90. const pipelineInfo = await circleCIRequest(pipelineInfoUrl, 'GET');
  91. switch (pipelineInfo.state) {
  92. case 'created': {
  93. const workflows = await circleCIRequest(`${pipelineInfoUrl}/workflow`, 'GET');
  94. // The logic below expects three workflow.items: publish, lint, & setup
  95. if (workflows.items.length === 3) {
  96. workflowId = workflows.items.find(item => item.name.includes('publish')).id;
  97. break;
  98. }
  99. console.log('Unxpected number of workflows, response was:', workflows);
  100. workflowId = -1;
  101. break;
  102. }
  103. case 'error': {
  104. console.log('Error retrieving workflows, response was:', pipelineInfo);
  105. workflowId = -1;
  106. break;
  107. }
  108. }
  109. await new Promise(resolve => setTimeout(resolve, CIRCLECI_WAIT_TIME));
  110. }
  111. return workflowId;
  112. }
  113. async function getCircleCIJobNumber (workflowId) {
  114. const jobInfoUrl = `https://circleci.com/api/v2/workflow/${workflowId}/job`;
  115. let jobNumber = 0;
  116. while (jobNumber === 0) {
  117. const jobInfo = await circleCIRequest(jobInfoUrl, 'GET');
  118. if (!jobInfo.items) {
  119. continue;
  120. }
  121. if (jobInfo.items.length !== 1) {
  122. console.log('Unxpected number of jobs, response was:', jobInfo);
  123. jobNumber = -1;
  124. break;
  125. }
  126. switch (jobInfo.items[0].status) {
  127. case 'not_running':
  128. case 'queued':
  129. case 'running': {
  130. if (jobInfo.items[0].job_number && !isNaN(jobInfo.items[0].job_number)) {
  131. jobNumber = jobInfo.items[0].job_number;
  132. }
  133. break;
  134. }
  135. case 'canceled':
  136. case 'error':
  137. case 'infrastructure_fail':
  138. case 'timedout':
  139. case 'not_run':
  140. case 'failed': {
  141. console.log(`Error job returned a status of ${jobInfo.items[0].status}, response was:`, jobInfo);
  142. jobNumber = -1;
  143. break;
  144. }
  145. }
  146. await new Promise(resolve => setTimeout(resolve, CIRCLECI_WAIT_TIME));
  147. }
  148. return jobNumber;
  149. }
  150. async function circleCIRequest (url, method, requestBody) {
  151. return makeRequest({
  152. auth: {
  153. username: process.env.CIRCLE_TOKEN,
  154. password: ''
  155. },
  156. method,
  157. url,
  158. headers: {
  159. 'Content-Type': 'application/json',
  160. Accept: 'application/json'
  161. },
  162. body: requestBody ? JSON.stringify(requestBody) : null
  163. }, true).catch(err => {
  164. console.log('Error calling CircleCI:', err);
  165. });
  166. }
  167. function buildAppVeyor (targetBranch, options) {
  168. const validJobs = Object.keys(appVeyorJobs);
  169. if (options.job) {
  170. assert(validJobs.includes(options.job), `Unknown AppVeyor CI job name: ${options.job}. Valid values are: ${validJobs}.`);
  171. callAppVeyor(targetBranch, options.job, options);
  172. } else {
  173. validJobs.forEach((job) => callAppVeyor(targetBranch, job, options));
  174. }
  175. }
  176. async function callAppVeyor (targetBranch, job, options) {
  177. console.log(`Triggering AppVeyor to run build job: ${job} on branch: ${targetBranch} with release flag.`);
  178. const environmentVariables = {
  179. ELECTRON_RELEASE: 1,
  180. APPVEYOR_BUILD_WORKER_CLOUD: 'electronhq-16-core'
  181. };
  182. if (!options.ghRelease) {
  183. environmentVariables.UPLOAD_TO_STORAGE = 1;
  184. }
  185. const requestOpts = {
  186. url: BUILD_APPVEYOR_URL,
  187. auth: {
  188. bearer: process.env.APPVEYOR_CLOUD_TOKEN
  189. },
  190. headers: {
  191. 'Content-Type': 'application/json'
  192. },
  193. body: JSON.stringify({
  194. accountName: 'electron-bot',
  195. projectSlug: appVeyorJobs[job],
  196. branch: targetBranch,
  197. commitId: options.commit || undefined,
  198. environmentVariables
  199. }),
  200. method: 'POST'
  201. };
  202. jobRequestedCount++;
  203. try {
  204. const { version } = await makeRequest(requestOpts, true);
  205. const buildUrl = `https://ci.appveyor.com/project/electron-bot/${appVeyorJobs[job]}/build/${version}`;
  206. console.log(`AppVeyor release build request for ${job} successful. Check build status at ${buildUrl}`);
  207. } catch (err) {
  208. console.log('Could not call AppVeyor: ', err);
  209. }
  210. }
  211. function buildCircleCI (targetBranch, options) {
  212. if (options.job) {
  213. assert(circleCIPublishWorkflows.includes(options.job), `Unknown CircleCI workflow name: ${options.job}. Valid values are: ${circleCIPublishWorkflows}.`);
  214. circleCIcall(targetBranch, options.job, options);
  215. } else {
  216. assert(!options.arch, 'Cannot provide a single architecture while building all workflows, please specify a single workflow via --workflow');
  217. options.runningPublishWorkflows = true;
  218. circleCIPublishWorkflows.forEach((job) => circleCIcall(targetBranch, job, options));
  219. }
  220. }
  221. function runRelease (targetBranch, options) {
  222. if (options.ci) {
  223. switch (options.ci) {
  224. case 'CircleCI': {
  225. buildCircleCI(targetBranch, options);
  226. break;
  227. }
  228. case 'AppVeyor': {
  229. buildAppVeyor(targetBranch, options);
  230. break;
  231. }
  232. default: {
  233. console.log(`Error! Unknown CI: ${options.ci}.`);
  234. process.exit(1);
  235. }
  236. }
  237. } else {
  238. buildCircleCI(targetBranch, options);
  239. buildAppVeyor(targetBranch, options);
  240. }
  241. console.log(`${jobRequestedCount} jobs were requested.`);
  242. }
  243. module.exports = runRelease;
  244. if (require.main === module) {
  245. const args = require('minimist')(process.argv.slice(2), {
  246. boolean: ['ghRelease']
  247. });
  248. const targetBranch = args._[0];
  249. if (args._.length < 1) {
  250. console.log(`Trigger CI to build release builds of electron.
  251. Usage: ci-release-build.js [--job=CI_JOB_NAME] [--arch=INDIVIDUAL_ARCH] [--ci=CircleCI|AppVeyor]
  252. [--ghRelease] [--circleBuildNum=xxx] [--appveyorJobId=xxx] [--commit=sha] TARGET_BRANCH
  253. `);
  254. process.exit(0);
  255. }
  256. runRelease(targetBranch, args);
  257. }