diff --git a/script/release/ci-release-build.js b/script/release/ci-release-build.js index 6b5c31ec8b49..9e120ce2bbc8 100644 --- a/script/release/ci-release-build.js +++ b/script/release/ci-release-build.js @@ -3,9 +3,18 @@ if (!process.env.CI) require('dotenv-safe').load(); const assert = require('node:assert'); const got = require('got'); +const { Octokit } = require('@octokit/rest'); +const octokit = new Octokit({ + auth: process.env.ELECTRON_GITHUB_TOKEN +}); + const BUILD_APPVEYOR_URL = 'https://ci.appveyor.com/api/builds'; const CIRCLECI_PIPELINE_URL = 'https://circleci.com/api/v2/project/gh/electron/electron/pipeline'; +const GH_ACTIONS_PIPELINE_URL = 'https://github.com/electron/electron/actions'; +const GH_ACTIONS_API_URL = '/repos/electron/electron/actions'; + const CIRCLECI_WAIT_TIME = process.env.CIRCLECI_WAIT_TIME || 30000; +const GH_ACTIONS_WAIT_TIME = process.env.GH_ACTIONS_WAIT_TIME || 30000; const appVeyorJobs = { 'electron-x64': 'electron-x64-release', @@ -23,6 +32,14 @@ const circleCIPublishIndividualArches = { 'linux-publish': ['arm', 'arm64', 'x64'] }; +const ghActionsPublishWorkflows = [ + 'macos-publish' +]; + +const ghActionsPublishIndividualArches = { + 'macos-publish': ['osx-x64', 'mas-x64', 'osx-arm64', 'mas-arm64'] +}; + let jobRequestedCount = 0; async function makeRequest ({ auth, username, password, url, headers, body, method }) { @@ -53,6 +70,65 @@ async function makeRequest ({ auth, username, password, url, headers, body, meth return JSON.parse(response.body); } +async function githubActionsCall (targetBranch, workflowName, options) { + console.log(`Triggering GitHub Actions to run build job: ${workflowName} on branch: ${targetBranch} with release flag.`); + const buildRequest = { + branch: targetBranch, + parameters: {} + }; + if (options.ghRelease) { + buildRequest.parameters['upload-to-storage'] = '0'; + } else { + buildRequest.parameters['upload-to-storage'] = '1'; + } + buildRequest.parameters[`run-${workflowName}`] = true; + if (options.arch) { + const validArches = ghActionsPublishIndividualArches[workflowName]; + assert(validArches.includes(options.arch), `Unknown GitHub Actions architecture "${options.arch}". Valid values are ${JSON.stringify(validArches)}`); + buildRequest.parameters['macos-publish-arch-limit'] = options.arch; + } + + jobRequestedCount++; + try { + const commits = await octokit.repos.listCommits({ + owner: 'electron', + repo: 'electron', + sha: targetBranch, + per_page: 5 + }); + if (!commits.data.length) { + console.error('Could not fetch most recent commits for GitHub Actions, returning early'); + } + + await octokit.request(`POST ${GH_ACTIONS_API_URL}/workflows/${workflowName}.yml/dispatches`, { + ref: buildRequest.branch, + inputs: { + ...buildRequest.parameters + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }); + + const runNumber = await getGitHubActionsRun(workflowName, commits.data[0].sha); + if (runNumber === -1) { + return; + } + + console.log(`GitHub Actions release build pipeline ${runNumber} for ${workflowName} triggered.`); + const runUrl = `${GH_ACTIONS_PIPELINE_URL}/runs/${runNumber}`; + + if (options.runningPublishWorkflows) { + console.log(`GitHub Actions release workflow request for ${workflowName} successful. Check ${runUrl} for status.`); + } else { + console.log(`GitHub Actions release build workflow running at ${GH_ACTIONS_PIPELINE_URL}/runs/${runNumber} for ${workflowName}.`); + console.log(`GitHub Actions release build request for ${workflowName} successful. Check ${runUrl} for status.`); + } + } catch (err) { + console.log('Error calling GitHub Actions: ', err); + } +} + async function circleCIcall (targetBranch, workflowName, options) { console.log(`Triggering CircleCI to run build job: ${workflowName} on branch: ${targetBranch} with release flag.`); const buildRequest = { @@ -167,6 +243,59 @@ async function getCircleCIJobNumber (workflowId) { return jobNumber; } +async function getGitHubActionsRun (workflowId, headCommit) { + let runNumber = 0; + let actionRun; + while (runNumber === 0) { + const actionsRuns = await octokit.request(`GET ${GH_ACTIONS_API_URL}/workflows/${workflowId}.yml/runs`, { + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }); + if (!actionsRuns.data.workflow_runs.length) { + console.log(`No current workflow_runs found for ${workflowId}, response was: ${actionsRuns.data.workflow_runs}`); + runNumber = -1; + break; + } + + for (const run of actionsRuns.data.workflow_runs) { + if (run.head_sha === headCommit) { + console.log(`GitHub Actions run ${run.html_url} found for ${headCommit}, waiting on status.`); + actionRun = run; + break; + } + } + + if (actionRun) { + switch (actionRun.status) { + case 'in_progress': + case 'pending': + case 'queued': + case 'requested': + case 'waiting': { + if (actionRun.id && !isNaN(actionRun.id)) { + console.log(`GitHub Actions run ${actionRun.status} for ${actionRun.html_url}.`); + runNumber = actionRun.id; + } + break; + } + case 'action_required': + case 'cancelled': + case 'failure': + case 'skipped': + case 'timed_out': + case 'failed': { + console.log(`Error workflow run returned a status of ${actionRun.status} for ${actionRun.html_url}`); + runNumber = -1; + break; + } + } + await new Promise(resolve => setTimeout(resolve, GH_ACTIONS_WAIT_TIME)); + } + } + return runNumber; +} + async function circleCIRequest (url, method, requestBody) { const requestOpts = { username: process.env.CIRCLE_TOKEN, @@ -194,18 +323,6 @@ async function circleCIRequest (url, method, requestBody) { }); } -function buildAppVeyor (targetBranch, options) { - const validJobs = Object.keys(appVeyorJobs); - if (options.job) { - assert(validJobs.includes(options.job), `Unknown AppVeyor CI job name: ${options.job}. Valid values are: ${validJobs}.`); - callAppVeyor(targetBranch, options.job, options); - } else { - for (const job of validJobs) { - callAppVeyor(targetBranch, job, options); - } - } -} - async function callAppVeyor (targetBranch, job, options) { console.log(`Triggering AppVeyor to run build job: ${job} on branch: ${targetBranch} with release flag.`); const environmentVariables = { @@ -252,6 +369,18 @@ async function callAppVeyor (targetBranch, job, options) { } } +function buildAppVeyor (targetBranch, options) { + const validJobs = Object.keys(appVeyorJobs); + if (options.job) { + assert(validJobs.includes(options.job), `Unknown AppVeyor CI job name: ${options.job}. Valid values are: ${validJobs}.`); + callAppVeyor(targetBranch, options.job, options); + } else { + for (const job of validJobs) { + callAppVeyor(targetBranch, job, options); + } + } +} + function buildCircleCI (targetBranch, options) { if (options.job) { assert(circleCIPublishWorkflows.includes(options.job), `Unknown CircleCI workflow name: ${options.job}. Valid values are: ${circleCIPublishWorkflows}.`); @@ -265,6 +394,19 @@ function buildCircleCI (targetBranch, options) { } } +function buildGHActions (targetBranch, options) { + if (options.job) { + assert(ghActionsPublishWorkflows.includes(options.job), `Unknown GitHub Actions workflow name: ${options.job}. Valid values are: ${ghActionsPublishWorkflows}.`); + githubActionsCall(targetBranch, options.job, options); + } else { + assert(!options.arch, 'Cannot provide a single architecture while building all workflows, please specify a single workflow via --workflow'); + options.runningPublishWorkflows = true; + for (const job of ghActionsPublishWorkflows) { + githubActionsCall(targetBranch, job, options); + } + } +} + function runRelease (targetBranch, options) { if (options.ci) { switch (options.ci) { @@ -272,6 +414,10 @@ function runRelease (targetBranch, options) { buildCircleCI(targetBranch, options); break; } + case 'GitHubActions': { + buildGHActions(targetBranch, options); + break; + } case 'AppVeyor': { buildAppVeyor(targetBranch, options); break; @@ -284,6 +430,8 @@ function runRelease (targetBranch, options) { } else { buildCircleCI(targetBranch, options); buildAppVeyor(targetBranch, options); + // TODO(vertedinde): Enable GH Actions in defaults when ready + // buildGHActions(targetBranch, options); } console.log(`${jobRequestedCount} jobs were requested.`); } @@ -297,7 +445,7 @@ if (require.main === module) { const targetBranch = args._[0]; if (args._.length < 1) { console.log(`Trigger CI to build release builds of electron. - Usage: ci-release-build.js [--job=CI_JOB_NAME] [--arch=INDIVIDUAL_ARCH] [--ci=CircleCI|AppVeyor] + Usage: ci-release-build.js [--job=CI_JOB_NAME] [--arch=INDIVIDUAL_ARCH] [--ci=CircleCI|AppVeyor|GitHubActions] [--ghRelease] [--circleBuildNum=xxx] [--appveyorJobId=xxx] [--commit=sha] TARGET_BRANCH `); process.exit(0);