Something went wrong. Try again.
What happens if I push all of nixpkgs
Something went wrong. Try again.
11 kB · 321 lines
JavaScript
at staging-next
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322module.exports = async ({ github, context, core, dry, cherryPicks }) => { const { execFileSync } = require('node:child_process') const { classify } = require('../supportedBranches.js') const withRateLimit = require('./withRateLimit.js') const { dismissReviews, postReview } = require('./reviews.js')
await withRateLimit({ github, core }, async (stats) => { stats.prs = 1
const pull_number = context.payload.pull_request.number
const job_url = context.runId && ( await github.paginate(github.rest.actions.listJobsForWorkflowRun, { ...context.repo, run_id: context.runId, per_page: 100, }) ).find(({ name }) => name.endsWith('Check / commits')).html_url + '?pr=' + pull_number
async function extract({ sha, commit }) { const noCherryPick = Array.from( commit.message.matchAll(/^Not-cherry-picked-because: (.*)$/gm), ).at(0)
if (noCherryPick) return { sha, commit, severity: 'important', message: `${sha} is not a cherry-pick, because: ${noCherryPick[1]}. Please review this commit manually.`, type: 'no-cherry-pick', }
// Using the last line with "cherry" + hash, because a chained backport // can result in multiple of those lines. Only the last one counts. const cherry = Array.from( commit.message.matchAll(/cherry.*([0-9a-f]{40})/g), ).at(-1)
if (!cherry) return { sha, commit, severity: 'warning', message: `Couldn't locate original commit hash in message of ${sha}.`, type: 'no-commit-hash', }
const original_sha = cherry[1]
let branches try { branches = ( await github.request({ // This is an undocumented endpoint to fetch the branches a commit is part of. // There is no equivalent in neither the REST nor the GraphQL API. // The endpoint itself is unlikely to go away, because GitHub uses it to display // the list of branches on the detail page of a commit. url: `https://github.com/${context.repo.owner}/${context.repo.repo}/branch_commits/${original_sha}`, headers: { accept: 'application/json', }, }) ).data.branches .map(({ branch }) => branch) .filter((branch) => classify(branch).type.includes('development')) } catch (e) { // For some unknown reason a 404 error comes back as 500 without any more details in a GitHub Actions runner. // Ignore these to return a regular error message below. if (![404, 500].includes(e.status)) throw e } if (!branches?.length) return { sha, commit, severity: 'error', message: `${original_sha} given in ${sha} not found in any pickable branch.`, }
return { sha, commit, original_sha, } }
function diff({ sha, commit, original_sha }) { const diff = execFileSync('git', [ '-C', __dirname, 'range-diff', '--no-color', '--ignore-all-space', '--no-notes', // 100 means "any change will be reported"; 0 means "no change will be reported" '--creation-factor=100', `${original_sha}~..${original_sha}`, `${sha}~..${sha}`, ]) .toString() .split('\n') // First line contains commit SHAs, which we'll print separately. .slice(1) // # The output of `git range-diff` is indented with 4 spaces, but we'll control indentation manually. .map((line) => line.replace(/^ {4}/, ''))
if (!diff.some((line) => line.match(/^[+-]{2}/))) return { sha, commit, severity: 'info', message: `✔ ${original_sha} is highly similar to ${sha}.`, }
const colored_diff = execFileSync('git', [ '-C', __dirname, 'range-diff', '--color', '--no-notes', '--creation-factor=100', `${original_sha}~..${original_sha}`, `${sha}~..${sha}`, ]).toString()
return { sha, commit, diff, colored_diff, severity: 'warning', message: `Difference between ${sha} and original ${original_sha} may warrant inspection.`, type: 'diff', } }
// For now we short-circuit the list of commits when cherryPicks should not be checked. // This will not run any checks, but still trigger the "dismiss reviews" part below. const commits = !cherryPicks ? [] : await github.paginate(github.rest.pulls.listCommits, { ...context.repo, pull_number, })
const extracted = await Promise.all(commits.map(extract))
const fetch = extracted .filter(({ severity }) => !severity) .flatMap(({ sha, original_sha }) => [sha, original_sha])
if (fetch.length > 0) { // Fetching all commits we need for diff at once is much faster than any other method. execFileSync('git', [ '-C', __dirname, 'fetch', '--depth=2', 'origin', ...fetch, ]) }
const results = extracted.map((result) => result.severity ? result : diff(result), )
// Log all results without truncation, with better highlighting and all whitespace changes to the job log. results.forEach(({ sha, commit, severity, message, colored_diff }) => { core.startGroup(`Commit ${sha}`) core.info(`Author: ${commit.author.name} ${commit.author.email}`) core.info(`Date: ${new Date(commit.author.date)}`) switch (severity) { case 'error': core.error(message) break case 'warning': core.warning(message) break default: core.info(message) } core.endGroup() if (colored_diff) core.info(colored_diff) })
// Only create step summary below in case of warnings or errors. // Also clean up older reviews, when all checks are good now. // An empty results array will always trigger this condition, which is helpful // to clean up reviews created by the prepare step when on the wrong branch. if (results.every(({ severity }) => severity === 'info')) { await dismissReviews({ github, context, dry }) return }
// In the case of "error" severity, we also fail the job. // Those should be considered blocking and not be dismissable via review. if (results.some(({ severity }) => severity === 'error')) process.exitCode = 1
core.summary.addRaw( 'This report is automatically generated by the `PR / Check / cherry-pick` CI workflow.', true, ) core.summary.addEOL() core.summary.addRaw( "Some of the commits in this PR require the author's and reviewer's attention.", true, ) core.summary.addEOL()
if (results.some(({ type }) => type === 'no-commit-hash')) { core.summary.addRaw( 'Please follow the [backporting guidelines](https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md#how-to-backport-pull-requests) and cherry-pick with the `-x` flag.', true, ) core.summary.addRaw( 'This requires changes to the unstable `master` and `staging` branches first, before backporting them.', true, ) core.summary.addEOL() core.summary.addRaw( 'Occasionally, commits are not cherry-picked at all, for example when updating minor versions of packages which have already advanced to the next major on unstable.', true, ) core.summary.addRaw( 'These commits can optionally be marked with a `Not-cherry-picked-because: <reason>` footer.', true, ) core.summary.addEOL() }
if (results.some(({ type }) => type === 'diff')) { core.summary.addRaw( 'Sometimes it is not possible to cherry-pick exactly the same patch.', true, ) core.summary.addRaw( 'This most frequently happens when resolving merge conflicts.', true, ) core.summary.addRaw( 'The range-diff will help to review the resolution of conflicts.', true, ) core.summary.addEOL() }
core.summary.addRaw( 'If you need to merge this PR despite the warnings, please [dismiss](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/dismissing-a-pull-request-review) this review shortly before merging.', true, )
results.forEach(({ severity, message, diff }) => { if (severity === 'info') return
// The docs for markdown alerts only show examples with markdown blockquote syntax, like this: // > [!WARNING] // > message // However, our testing shows that this also works with a `<blockquote>` html tag, as long as there // is an empty line: // <blockquote> // // [!WARNING] // message // </blockquote> // Whether this is intended or just an implementation detail is unclear. core.summary.addRaw('<blockquote>') core.summary.addRaw( `\n\n[!${{ important: 'IMPORTANT', warning: 'WARNING', error: 'CAUTION' }[severity]}]`, true, ) core.summary.addRaw(`${message}`, true)
if (diff) { // Limit the output to 10k bytes and remove the last, potentially incomplete line, because GitHub // comments are limited in length. The value of 10k is arbitrary with the assumption, that after // the range-diff becomes a certain size, a reviewer is better off reviewing the regular diff in // GitHub's UI anyway, thus treating the commit as "new" and not cherry-picked. // Note: if multiple commits are close to the limit, this approach could still lead to a comment // that's too long. We think this is unlikely to happen, and so don't deal with it explicitly. const truncated = [] let total_length = 0 for (line of diff) { total_length += line.length if (total_length > 10000) { truncated.push('', '[...truncated...]') break } else { truncated.push(line) } }
core.summary.addRaw('<details><summary>Show diff</summary>') core.summary.addRaw('\n\n``````````diff', true) core.summary.addRaw(truncated.join('\n'), true) core.summary.addRaw('``````````', true) core.summary.addRaw('</details>') }
core.summary.addRaw('</blockquote>') })
if (job_url) core.summary.addRaw( `\n\n_Hint: The full diffs are also available in the [runner logs](${job_url}) with slightly better highlighting._`, )
const body = core.summary.stringify() core.summary.write()
// Posting a review could fail for very long comments. This can only happen with // multiple commits all hitting the truncation limit for the diff. If you ever hit // this case, consider just splitting up those commits into multiple PRs. await postReview({ github, context, core, dry, body }) })}