ci-release-build.js 9.7 KB

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