ci-release-build.js 12 KB

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