run-release-ci-jobs.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. import { Octokit } from '@octokit/rest';
  2. import got, { OptionsOfTextResponseBody } from 'got';
  3. import * as assert from 'node:assert';
  4. import { createGitHubTokenStrategy } from './github-token';
  5. import { ELECTRON_ORG, ELECTRON_REPO } from './types';
  6. const octokit = new Octokit({
  7. authStrategy: createGitHubTokenStrategy(ELECTRON_REPO)
  8. });
  9. const BUILD_APPVEYOR_URL = 'https://ci.appveyor.com/api/builds';
  10. const GH_ACTIONS_PIPELINE_URL = 'https://github.com/electron/electron/actions';
  11. const GH_ACTIONS_WAIT_TIME = process.env.GH_ACTIONS_WAIT_TIME ? parseInt(process.env.GH_ACTIONS_WAIT_TIME, 10) : 30000;
  12. const appVeyorJobs = {
  13. 'electron-x64': 'electron-x64-release',
  14. 'electron-ia32': 'electron-ia32-release',
  15. 'electron-woa': 'electron-woa-release'
  16. };
  17. const ghActionsPublishWorkflows = [
  18. 'linux-publish',
  19. 'macos-publish'
  20. ] as const;
  21. let jobRequestedCount = 0;
  22. type ReleaseBuildRequestOptions = {
  23. auth?: {
  24. bearer?: string;
  25. };
  26. url: string;
  27. headers: Record<string, string>;
  28. body: string,
  29. method: 'GET' | 'POST';
  30. }
  31. async function makeRequest ({ auth, url, headers, body, method }: ReleaseBuildRequestOptions) {
  32. const clonedHeaders = {
  33. ...(headers || {})
  34. };
  35. if (auth?.bearer) {
  36. clonedHeaders.Authorization = `Bearer ${auth.bearer}`;
  37. }
  38. const options: OptionsOfTextResponseBody = {
  39. headers: clonedHeaders,
  40. body,
  41. method
  42. };
  43. const response = await got(url, options);
  44. if (response.statusCode < 200 || response.statusCode >= 300) {
  45. console.error('Error: ', `(status ${response.statusCode})`, response.body);
  46. throw new Error(`Unexpected status code ${response.statusCode} from ${url}`);
  47. }
  48. return JSON.parse(response.body);
  49. }
  50. type GitHubActionsCallOptions = {
  51. ghRelease?: boolean;
  52. newVersion: string;
  53. runningPublishWorkflows?: boolean;
  54. }
  55. async function githubActionsCall (targetBranch: string, workflowName: string, options: GitHubActionsCallOptions) {
  56. console.log(`Triggering GitHub Actions to run build job: ${workflowName} on branch: ${targetBranch} with release flag.`);
  57. const buildRequest = {
  58. branch: targetBranch,
  59. parameters: {} as Record<string, string | boolean>
  60. };
  61. if (options.ghRelease) {
  62. buildRequest.parameters['upload-to-storage'] = '0';
  63. } else {
  64. buildRequest.parameters['upload-to-storage'] = '1';
  65. }
  66. buildRequest.parameters[`run-${workflowName}`] = true;
  67. jobRequestedCount++;
  68. try {
  69. const commits = await octokit.repos.listCommits({
  70. owner: ELECTRON_ORG,
  71. repo: ELECTRON_REPO,
  72. sha: targetBranch,
  73. per_page: 5
  74. });
  75. if (!commits.data.length) {
  76. console.error('Could not fetch most recent commits for GitHub Actions, returning early');
  77. }
  78. await octokit.actions.createWorkflowDispatch({
  79. repo: ELECTRON_REPO,
  80. owner: ELECTRON_ORG,
  81. workflow_id: `${workflowName}.yml`,
  82. ref: `refs/tags/${options.newVersion}`,
  83. inputs: {
  84. ...buildRequest.parameters
  85. }
  86. });
  87. const runNumber = await getGitHubActionsRun(workflowName, commits.data[0].sha);
  88. if (runNumber === -1) {
  89. return;
  90. }
  91. console.log(`GitHub Actions release build pipeline ${runNumber} for ${workflowName} triggered.`);
  92. const runUrl = `${GH_ACTIONS_PIPELINE_URL}/runs/${runNumber}`;
  93. if (options.runningPublishWorkflows) {
  94. console.log(`GitHub Actions release workflow request for ${workflowName} successful. Check ${runUrl} for status.`);
  95. } else {
  96. console.log(`GitHub Actions release build workflow running at ${GH_ACTIONS_PIPELINE_URL}/runs/${runNumber} for ${workflowName}.`);
  97. console.log(`GitHub Actions release build request for ${workflowName} successful. Check ${runUrl} for status.`);
  98. }
  99. } catch (err) {
  100. console.log('Error calling GitHub Actions: ', err);
  101. }
  102. }
  103. async function getGitHubActionsRun (workflowName: string, headCommit: string) {
  104. let runNumber = 0;
  105. let actionRun;
  106. while (runNumber === 0) {
  107. const actionsRuns = await octokit.actions.listWorkflowRuns({
  108. repo: ELECTRON_REPO,
  109. owner: ELECTRON_ORG,
  110. workflow_id: `${workflowName}.yml`
  111. });
  112. if (!actionsRuns.data.workflow_runs.length) {
  113. console.log(`No current workflow_runs found for ${workflowName}, response was: ${actionsRuns.data.workflow_runs}`);
  114. runNumber = -1;
  115. break;
  116. }
  117. for (const run of actionsRuns.data.workflow_runs) {
  118. if (run.head_sha === headCommit) {
  119. console.log(`GitHub Actions run ${run.html_url} found for ${headCommit}, waiting on status.`);
  120. actionRun = run;
  121. break;
  122. }
  123. }
  124. if (actionRun) {
  125. switch (actionRun.status) {
  126. case 'in_progress':
  127. case 'pending':
  128. case 'queued':
  129. case 'requested':
  130. case 'waiting': {
  131. if (actionRun.id && !isNaN(actionRun.id)) {
  132. console.log(`GitHub Actions run ${actionRun.status} for ${actionRun.html_url}.`);
  133. runNumber = actionRun.id;
  134. }
  135. break;
  136. }
  137. case 'action_required':
  138. case 'cancelled':
  139. case 'failure':
  140. case 'skipped':
  141. case 'timed_out':
  142. case 'failed': {
  143. console.log(`Error workflow run returned a status of ${actionRun.status} for ${actionRun.html_url}`);
  144. runNumber = -1;
  145. break;
  146. }
  147. }
  148. await new Promise(resolve => setTimeout(resolve, GH_ACTIONS_WAIT_TIME));
  149. }
  150. }
  151. return runNumber;
  152. }
  153. type AppVeyorCallOptions = {
  154. ghRelease?: boolean;
  155. commit?: string;
  156. }
  157. async function callAppVeyor (targetBranch: string, job: keyof typeof appVeyorJobs, options: AppVeyorCallOptions) {
  158. console.log(`Triggering AppVeyor to run build job: ${job} on branch: ${targetBranch} with release flag.`);
  159. const environmentVariables: Record<string, string | number> = {
  160. ELECTRON_RELEASE: 1,
  161. APPVEYOR_BUILD_WORKER_CLOUD: 'electronhq-16-core'
  162. };
  163. if (!options.ghRelease) {
  164. environmentVariables.UPLOAD_TO_STORAGE = 1;
  165. }
  166. const requestOpts = {
  167. url: BUILD_APPVEYOR_URL,
  168. auth: {
  169. bearer: process.env.APPVEYOR_CLOUD_TOKEN
  170. },
  171. headers: {
  172. 'Content-Type': 'application/json'
  173. },
  174. body: JSON.stringify({
  175. accountName: 'electron-bot',
  176. projectSlug: appVeyorJobs[job],
  177. branch: targetBranch,
  178. commitId: options.commit || undefined,
  179. environmentVariables
  180. }),
  181. method: 'POST'
  182. } as const;
  183. jobRequestedCount++;
  184. try {
  185. const { version } = await makeRequest(requestOpts);
  186. const buildUrl = `https://ci.appveyor.com/project/electron-bot/${appVeyorJobs[job]}/build/${version}`;
  187. console.log(`AppVeyor release build request for ${job} successful. Check build status at ${buildUrl}`);
  188. } catch (err: any) {
  189. if (err.response?.body) {
  190. console.error('Could not call AppVeyor: ', {
  191. statusCode: err.response.statusCode,
  192. body: JSON.parse(err.response.body)
  193. });
  194. } else {
  195. console.error('Error calling AppVeyor:', err);
  196. }
  197. }
  198. }
  199. type BuildAppVeyorOptions = {
  200. job?: keyof typeof appVeyorJobs;
  201. } & AppVeyorCallOptions;
  202. async function buildAppVeyor (targetBranch: string, options: BuildAppVeyorOptions) {
  203. const validJobs = Object.keys(appVeyorJobs) as (keyof typeof appVeyorJobs)[];
  204. if (options.job) {
  205. assert(validJobs.includes(options.job), `Unknown AppVeyor CI job name: ${options.job}. Valid values are: ${validJobs}.`);
  206. await callAppVeyor(targetBranch, options.job, options);
  207. } else {
  208. for (const job of validJobs) {
  209. await callAppVeyor(targetBranch, job, options);
  210. }
  211. }
  212. }
  213. type BuildGHActionsOptions = {
  214. job?: typeof ghActionsPublishWorkflows[number];
  215. arch?: string;
  216. } & GitHubActionsCallOptions;
  217. async function buildGHActions (targetBranch: string, options: BuildGHActionsOptions) {
  218. if (options.job) {
  219. assert(ghActionsPublishWorkflows.includes(options.job), `Unknown GitHub Actions workflow name: ${options.job}. Valid values are: ${ghActionsPublishWorkflows}.`);
  220. await githubActionsCall(targetBranch, options.job, options);
  221. } else {
  222. assert(!options.arch, 'Cannot provide a single architecture while building all workflows, please specify a single workflow via --workflow');
  223. options.runningPublishWorkflows = true;
  224. for (const job of ghActionsPublishWorkflows) {
  225. await githubActionsCall(targetBranch, job, options);
  226. }
  227. }
  228. }
  229. type RunReleaseOptions = ({
  230. ci: 'GitHubActions'
  231. } & BuildGHActionsOptions) | ({
  232. ci: 'AppVeyor'
  233. } & BuildAppVeyorOptions) | ({
  234. ci: undefined,
  235. } & BuildAppVeyorOptions & BuildGHActionsOptions);
  236. export async function runReleaseCIJobs (targetBranch: string, options: RunReleaseOptions) {
  237. if (options.ci) {
  238. switch (options.ci) {
  239. case 'GitHubActions': {
  240. await buildGHActions(targetBranch, options);
  241. break;
  242. }
  243. case 'AppVeyor': {
  244. await buildAppVeyor(targetBranch, options);
  245. break;
  246. }
  247. default: {
  248. console.log(`Error! Unknown CI: ${(options as any).ci}.`);
  249. process.exit(1);
  250. }
  251. }
  252. } else {
  253. await Promise.all([
  254. buildAppVeyor(targetBranch, options),
  255. buildGHActions(targetBranch, options)
  256. ]);
  257. }
  258. console.log(`${jobRequestedCount} jobs were requested.`);
  259. }