Files
cypress/scripts/semantic-commits/get-binary-release-data.js

222 lines
6.7 KiB
JavaScript

const Bluebird = require('bluebird')
const childProcess = require('child_process')
const _ = require('lodash')
const { Octokit } = require('@octokit/core')
const debugLib = require('debug')
const { getCurrentReleaseData } = require('./get-current-release-data')
const { getNextVersionForBinary } = require('../get-next-version')
const { getLinkedIssues } = require('./get-linked-issues')
const debug = debugLib('scripts:semantic-commits:get-binary-release-data')
const ensureAuth = () => {
if (!process.env.GH_TOKEN) {
throw new Error('The GH_TOKEN env is not set.')
}
}
/**
* Get the list of file names that have been added, deleted or changed since the git
* sha associated with the latest tag published on npm.
*
* @param {object} latestReleaseInfo - data of the latest tag published on npm
* @param {string} latestReleaseInfo.version - version of Cypress
* @param {string} latestReleaseInfo.commitDate - data of release
* @param {string} latestReleaseInfo.buildSha - git commit associated with published content
*/
const getChangedFilesSinceLastRelease = (latestReleaseInfo) => {
const stdout = childProcess.execSync(`git diff ${latestReleaseInfo.buildSha}.. --name-only`, { encoding: 'utf8' })
if (!stdout) {
console.log('no files changes since last release')
return []
}
return stdout.split('\n')
}
// returns number of milliseconds to wait for retry
const getRetryAfter = (headers) => {
// retry-after header is number of seconds to wait
if (headers['retry-after'] && headers['retry-after'] !== '0') {
return parseInt(headers['retry-after'], 10) * 1000
}
// x-ratelimit-reset header is the time in seconds since epoch after
// which to retry
if (headers['x-ratelimit-reset']) {
const epochSeconds = parseInt(headers['x-ratelimit-reset'], 10)
// turn it into milliseconds to wait and pad it by a second for good measure
return (epochSeconds - (Date.now() / 1000)) * 1000 + 1000
}
// otherwise, just wait a minute
return 6000
}
// https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#secondary-rate-limits
const parseRateLimit = (err) => {
if (err.status === 403 && err.response?.data?.message.includes('secondary rate limit')) {
const retryAfter = getRetryAfter(err.response?.headers)
return {
rateLimitHit: true,
retryAfter,
}
}
return {
rateLimitHit: false,
}
}
const fetchPullRequest = async (octokit, pullNumber) => {
try {
const { data: pullRequest } = await octokit.request('GET /repos/{owner}/{repo}/pulls/{pull_number}', {
owner: 'cypress-io',
repo: 'cypress',
pull_number: pullNumber,
})
return pullRequest
} catch (err) {
const { rateLimitHit, retryAfter } = parseRateLimit(err)
if (rateLimitHit) {
console.log(`Rate limit hit - Retry fetching PR #${pullNumber} after ${retryAfter}ms`)
return Bluebird.delay(retryAfter).then(() => fetchPullRequest(octokit, pullNumber))
}
throw err
}
}
/**
* Get the next release version given the semantic commits in the git history. Then using the commit history,
* determine which files have changed, list of PRs merged and issues resolved since the latest tag was
* published on npm. It also collects the list of commit data including the semantic type, PR and associated
* issues.
*
* @param {object} latestReleaseInfo - data of the latest tag published on npm
* @param {string} latestReleaseInfo.version - version of Cypress
* @param {string} latestReleaseInfo.commitDate - data of release
* @param {string} latestReleaseInfo.buildSha - git commit associated with published content
*/
const getReleaseData = async (latestReleaseInfo) => {
ensureAuth()
const octokit = new Octokit({ auth: process.env.GH_TOKEN })
let {
nextVersion,
commits: semanticCommits,
} = await getNextVersionForBinary()
semanticCommits = _.uniqBy(semanticCommits, (commit) => commit.header)
const changedFiles = await getChangedFilesSinceLastRelease(latestReleaseInfo)
const issuesInRelease = []
const prsInRelease = []
const commits = []
await Bluebird.each(semanticCommits, (async (semanticResult) => {
if (!semanticResult) return
const { type: semanticType, references } = semanticResult
if (!references.length) {
console.log('Commit does not have an associated pull request number...')
return
}
const refs = references.filter((r) => !r.raw.includes('revert #'))
if (!refs.length) {
console.log('Commit does not have an associated pull request number...')
return
}
for (const [idx, ref] of refs.entries()) {
let pullRequest
try {
pullRequest = await fetchPullRequest(octokit, ref.issue)
debug(`Pull request #${ref.issue} found!`)
} catch (err) {
debug(`Error while fetching PR #${ ref.issue}:`, err)
// If we have tried to fetch all other references and all of them failed,
// print the failure of the last reference and exit
if (idx === refs.length) {
debug(`all references exhausted and no PR could be found!`)
console.log(`Error while fetching PR #${ ref.issue}:`, err)
throw err
} else {
// otherwise, we still might be able to link a PR to the commit
debug(`Other references need to be tried. Continuing to see if we can find a corresponding pull request.`)
}
continue
}
const associatedIssues = pullRequest.body ? getLinkedIssues(pullRequest.body) : []
commits.push({
commitMessage: semanticResult.header,
semanticType,
prNumber: ref.issue,
associatedIssues,
})
prsInRelease.push(`https://github.com/cypress-io/cypress/pull/${ref.issue}`)
associatedIssues.forEach((issueNumber) => {
issuesInRelease.push(`https://github.com/cypress-io/cypress/issues/${issueNumber}`)
})
// since we found our pull request, we don't need to check the rest of the references
break
}
}))
console.log('Next release version is', nextVersion)
console.log(`${prsInRelease.length} pull requests have merged since ${latestReleaseInfo.version} was released.`)
prsInRelease.forEach((link) => {
console.log(' -', link)
})
console.log(`${issuesInRelease.length} issues addressed since ${latestReleaseInfo.version} was released.`)
issuesInRelease.forEach((link) => {
console.log(' -', link)
})
return {
nextVersion,
changedFiles,
commits,
issuesInRelease,
prsInRelease,
}
}
if (require.main !== module) {
module.exports = {
getReleaseData,
}
return
}
(async () => {
const latestReleaseInfo = await getCurrentReleaseData()
await getReleaseData(latestReleaseInfo)
})()