diff --git a/server.js b/server.js index eab9e50..ec840b0 100644 --- a/server.js +++ b/server.js @@ -14,6 +14,7 @@ const { } = require('./utils/needs-manual-prs'); const { fetchInitiator, + getReleaseBranches, getSemverForCommitRange, getSupportedBranches, isInvalidBranch, @@ -49,7 +50,7 @@ app.get('/verify-semver', async (req, res) => { const { branch } = req.query; - const branches = await getSupportedBranches(); + const branches = await getReleaseBranches(); if (isInvalidBranch(branches, branch)) { res.status(400).json({ error: `${branch} is not a valid branch` }); return; @@ -76,7 +77,7 @@ app.get('/verify-semver', async (req, res) => { app.post('/verify-semver', verifySlackRequest, async (req, res) => { res.status(200).end(); - const branches = await getSupportedBranches(); + const branches = await getReleaseBranches(); const branch = req.body.text; const initiator = await fetchInitiator(req); @@ -127,7 +128,6 @@ app.post('/verify-semver', verifySlackRequest, async (req, res) => { // Check for pull requests targeting a specified release branch // that have not yet been merged. app.post('/unmerged', verifySlackRequest, async (req, res) => { - const branches = await getSupportedBranches(); const branch = req.body.text; const initiator = await fetchInitiator(req); @@ -135,7 +135,7 @@ app.post('/unmerged', verifySlackRequest, async (req, res) => { `${initiator.name} initiated unmerged audit for branch: ${branch}`, ); - if (branch !== 'all' && isInvalidBranch(branches, branch)) { + if (branch !== 'all' && isInvalidBranch(await getReleaseBranches(), branch)) { console.error(`${branch} is not a valid branch`); await postToSlack( { @@ -150,7 +150,8 @@ app.post('/unmerged', verifySlackRequest, async (req, res) => { console.log(`Auditing unmerged PRs on branch: ${branch}`); try { - const branchesToCheck = branch === 'all' ? branches : [branch]; + const branchesToCheck = + branch === 'all' ? await getSupportedBranches() : [branch]; let messages = []; for (const branch of branchesToCheck) { @@ -191,7 +192,6 @@ app.post('/unmerged', verifySlackRequest, async (req, res) => { // Check for pull requests which have been merged to main and labeled // with target/BRANCH_NAME that trop failed for and which still need manual backports. app.post('/needs-manual', verifySlackRequest, async (req, res) => { - const branches = await getSupportedBranches(); const REMIND = 'remind'; let [branch, author, remind] = req.body.text.split(' '); @@ -209,7 +209,7 @@ app.post('/needs-manual', verifySlackRequest, async (req, res) => { `${initiator.name} initiated needs-manual audit for branch: ${branch}`, ); - if (branch !== 'all' && isInvalidBranch(branches, branch)) { + if (branch !== 'all' && isInvalidBranch(await getReleaseBranches(), branch)) { console.error(`${branch} is not a valid branch`); await postToSlack( { @@ -241,7 +241,8 @@ app.post('/needs-manual', verifySlackRequest, async (req, res) => { } try { - const branchesToCheck = branch === 'all' ? branches : [branch]; + const branchesToCheck = + branch === 'all' ? await getSupportedBranches() : [branch]; let messages = []; for (const branch of branchesToCheck) { @@ -296,15 +297,16 @@ app.get('/unreleased', async (req, res) => { const { branch } = req.query; try { - const branches = await getSupportedBranches(); const result = {}; if (branch === 'all') { + const branches = await getSupportedBranches(); for (const b of branches) { const { commits } = await fetchUnreleasedCommits(b); result[b] = commits; } } else { + const branches = await getReleaseBranches(); if (isInvalidBranch(branches, branch)) { return res .status(400) @@ -326,13 +328,13 @@ app.get('/unreleased', async (req, res) => { // Check for commits which have been merged to a release branch but // not been released in a beta or stable. app.post('/unreleased', verifySlackRequest, async (req, res) => { - const branches = await getSupportedBranches(); const branch = req.body.text; const initiator = await fetchInitiator(req); // Allow for manual batch audit of all supported release branches. if (branch === 'all') { + const branches = await getSupportedBranches(); console.log( `${initiator.name} triggered audit for all supported release branches`, ); @@ -368,6 +370,8 @@ app.post('/unreleased', verifySlackRequest, async (req, res) => { `${initiator.name} initiated unreleased commit audit for branch: ${branch}`, ); + const branches = await getReleaseBranches(); + if (isInvalidBranch(branches, branch)) { console.error(`${branch} is not a valid branch`); await postToSlack( @@ -408,7 +412,7 @@ app.post('/unreleased', verifySlackRequest, async (req, res) => { // Combines checks for all PRs that either need manual backport to a given // release line or which are targeting said line and haven't been merged. app.post('/audit-pre-release', verifySlackRequest, async (req, res) => { - const branches = await getSupportedBranches(); + const branches = await getReleaseBranches(); const branch = req.body.text; const initiator = await fetchInitiator(req); diff --git a/test/gh-api-calls.js b/test/gh-api-calls.js index 29488b7..c3fc3d8 100644 --- a/test/gh-api-calls.js +++ b/test/gh-api-calls.js @@ -4,6 +4,7 @@ const { parseArgs } = require('node:util'); const { fetchUnreleasedCommits } = require('../utils/unreleased-commits'); const { + getReleaseBranches, getSemverForCommitRange, getSupportedBranches, releaseIsDraft, @@ -52,6 +53,18 @@ describe('API tests', () => { assert.ok(Array.isArray(prs)); }); + it('can get release branches', async () => { + const branches = await getReleaseBranches(); + assert.ok(Array.isArray(branches)); + assert.strictEqual(branches.length, 10); + }); + + it('can get more release branches', async () => { + const branches = await getReleaseBranches(15); + assert.ok(Array.isArray(branches)); + assert.strictEqual(branches.length, 15); + }); + it('can get supported branches', async () => { const branches = await getSupportedBranches(); assert.ok(Array.isArray(branches)); diff --git a/utils/helpers.js b/utils/helpers.js index 1db1e60..cccac14 100644 --- a/utils/helpers.js +++ b/utils/helpers.js @@ -122,21 +122,78 @@ async function releaseIsDraft(tag) { } } -// Fetch an array of the currently supported branches. -async function getSupportedBranches() { - const resp = await fetch( - 'https://releases.electronjs.org/schedule.json?eolGracePeriod=7', - ); +async function fetchSchedule() { + const resp = await fetch('https://releases.electronjs.org/schedule.json'); if (!resp.ok) { throw new Error( - `Failed to fetch supported branches: ${resp.status} ${resp.statusText}`, + `Failed to fetch release branches: ${resp.status} ${resp.statusText}`, ); } const schedule = await resp.json(); + return Object.values(schedule); +} + +async function branchExists(branch) { + const octokit = await getOctokit(); + + try { + await octokit.rest.repos.getBranch({ + owner: ORGANIZATION_NAME, + repo: REPO_NAME, + branch, + }); + } catch (err) { + // A 404 means the branch genuinely doesn't exist + if (err.status === 404) { + return false; + } + throw err; + } + + return true; +} - return Object.values(schedule) +async function checkForNewReleaseBranch(latestVersion) { + const latestMajor = latestVersion.split('.')[0]; + const nextMajorBranch = `${latestMajor}-x-y`; + + if (await branchExists(nextMajorBranch)) { + return nextMajorBranch; + } + + return null; +} + +// Fetch an array of the currently supported branches. +async function getSupportedBranches() { + const schedule = await fetchSchedule(); + const branches = schedule .filter(({ status }) => ['prerelease', 'stable'].includes(status)) .map(({ branch }) => branch); + + // Schedule is cached, so check GH for a new release branch + const newReleaseBranch = await checkForNewReleaseBranch(schedule[0].version); + if (newReleaseBranch) { + branches.unshift(newReleaseBranch); + branches.pop(); // Knock out the last stable branch if we have a new release branch + } + + return branches; +} + +// Fetch an array of newest N release branches +async function getReleaseBranches(n = 10) { + const schedule = await fetchSchedule(); + const branches = schedule.slice(0, n).map(({ branch }) => branch); + + // Schedule is cached, so check GH for a new release branch + const newReleaseBranch = await checkForNewReleaseBranch(schedule[0].version); + if (newReleaseBranch) { + branches.splice(1, 0, newReleaseBranch); + branches.pop(); + } + + return branches; } // Post a message to a Slack workspace. @@ -215,6 +272,7 @@ function verifySlackRequest(req, res, next) { module.exports = { escapeSlackText, fetchInitiator, + getReleaseBranches, getSemverForCommitRange, getSupportedBranches, isInvalidBranch,