nixpkgs mirror (for testing) github.com/NixOS/nixpkgs
nix
at release-25.11 233 lines 9.6 kB view raw
1const { classify } = require('../supportedBranches.js') 2const { postReview, dismissReviews } = require('./reviews.js') 3const reviewKey = 'prepare' 4const supportedSystems = require('./supportedSystems.js') 5 6module.exports = async ({ github, context, core, dry }) => { 7 const pull_number = context.payload.pull_request.number 8 9 for (const retryInterval of [5, 10, 20, 40, 80]) { 10 core.info('Checking whether the pull request can be merged...') 11 const prInfo = ( 12 await github.rest.pulls.get({ 13 ...context.repo, 14 pull_number, 15 }) 16 ).data 17 18 if (prInfo.state !== 'open') throw new Error('PR is not open anymore.') 19 20 if (prInfo.mergeable == null) { 21 core.info( 22 `GitHub is still computing whether this PR can be merged, waiting ${retryInterval} seconds before trying again...`, 23 ) 24 await new Promise((resolve) => setTimeout(resolve, retryInterval * 1000)) 25 continue 26 } 27 28 const { base, head } = prInfo 29 30 const baseClassification = classify(base.ref) 31 core.setOutput('base', baseClassification) 32 console.log('base classification:', baseClassification) 33 34 const headClassification = 35 base.repo.full_name === head.repo.full_name 36 ? classify(head.ref) 37 : // PRs from forks are always considered WIP. 38 { type: ['wip'] } 39 core.setOutput('head', headClassification) 40 console.log('head classification:', headClassification) 41 42 if (baseClassification.type.includes('channel')) { 43 const { stable, version } = baseClassification 44 const correctBranch = stable ? `release-${version}` : 'master' 45 const body = [ 46 'The `nixos-*` and `nixpkgs-*` branches are pushed to by the channel release script and should not be merged into directly.', 47 '', 48 `Please target \`${correctBranch}\` instead.`, 49 ].join('\n') 50 51 await postReview({ github, context, core, dry, body, reviewKey }) 52 53 throw new Error('The PR targets a channel branch.') 54 } 55 56 if (headClassification.type.includes('wip')) { 57 // In the following, we look at the git history to determine the base branch that 58 // this Pull Request branched off of. This is *supposed* to be the branch that it 59 // merges into, but humans make mistakes. Once that happens we want to error out as 60 // early as possible. 61 62 // To determine the "real base", we are looking at the merge-base of primary development 63 // branches and the head of the PR. The merge-base which results in the least number of 64 // commits between that base and head is the real base. We can query for this via GitHub's 65 // REST API. There can be multiple candidates for the real base with the same number of 66 // commits. In this case we pick the "best" candidate by a fixed ordering of branches, 67 // as defined in ci/supportedBranches.js. 68 // 69 // These requests take a while, when comparing against the wrong release - they need 70 // to look at way more than 10k commits in that case. Thus, we try to minimize the 71 // number of requests across releases: 72 // - First, we look at the primary development branches only: master and release-xx.yy. 73 // The branch with the fewest commits gives us the release this PR belongs to. 74 // - We then compare this number against the relevant staging branches for this release 75 // to find the exact branch that this belongs to. 76 77 // All potential development branches 78 const branches = ( 79 await github.paginate(github.rest.repos.listBranches, { 80 ...context.repo, 81 per_page: 100, 82 }) 83 ).map(({ name }) => classify(name)) 84 85 // All stable primary development branches from latest to oldest. 86 const releases = branches 87 .filter(({ stable, type }) => type.includes('primary') && stable) 88 .sort((a, b) => b.version.localeCompare(a.version)) 89 90 async function mergeBase({ branch, order, version }) { 91 const { data } = await github.rest.repos.compareCommitsWithBasehead({ 92 ...context.repo, 93 basehead: `${branch}...${head.sha}`, 94 // Pagination for this endpoint is about the commits listed, which we don't care about. 95 per_page: 1, 96 // Taking the second page skips the list of files of this changeset. 97 page: 2, 98 }) 99 return { 100 branch, 101 order, 102 version, 103 commits: data.total_commits, 104 sha: data.merge_base_commit.sha, 105 } 106 } 107 108 // Multiple branches can be OK at the same time, if the PR was created of a merge-base, 109 // thus storing as array. 110 let candidates = [await mergeBase(classify('master'))] 111 for (const release of releases) { 112 const nextCandidate = await mergeBase(release) 113 if (candidates[0].commits === nextCandidate.commits) 114 candidates.push(nextCandidate) 115 if (candidates[0].commits > nextCandidate.commits) 116 candidates = [nextCandidate] 117 // The number 10000 is principally arbitrary, but the GitHub API returns this value 118 // when the number of commits exceeds it in reality. The difference between two stable releases 119 // is certainly more than 10k commits, thus this works for us as well: If we're targeting 120 // a wrong release, the number *will* be 10000. 121 if (candidates[0].commits < 10000) break 122 } 123 124 core.info(`This PR is for NixOS ${candidates[0].version}.`) 125 126 // Secondary development branches for the selected version only. 127 const secondary = branches.filter( 128 ({ branch, type, version }) => 129 type.includes('secondary') && version === candidates[0].version, 130 ) 131 132 // Make sure that we always check the current target as well, even if its a WIP branch. 133 secondary.push(classify(base.ref)) 134 135 for (const branch of secondary) { 136 const nextCandidate = await mergeBase(branch) 137 if (candidates[0].commits === nextCandidate.commits) 138 candidates.push(nextCandidate) 139 if (candidates[0].commits > nextCandidate.commits) 140 candidates = [nextCandidate] 141 } 142 143 // If the current branch is among the candidates, this is always better than any other, 144 // thus sorting at -1. 145 candidates = candidates 146 .map((candidate) => 147 candidate.branch === base.ref 148 ? { ...candidate, order: -1 } 149 : candidate, 150 ) 151 .sort((a, b) => a.order - b.order) 152 153 const best = candidates.at(0) 154 155 core.info('The base branches for this PR are:') 156 core.info(`github: ${base.ref}`) 157 core.info( 158 `candidates: ${candidates.map(({ branch }) => branch).join(',')}`, 159 ) 160 core.info(`best candidate: ${best.branch}`) 161 162 if (best.branch !== base.ref) { 163 const current = await mergeBase(classify(base.ref)) 164 const body = [ 165 `The PR's base branch is set to \`${current.branch}\`, but ${current.commits === 10000 ? 'at least 10000' : current.commits - best.commits} commits from the \`${best.branch}\` branch are included. Make sure you know the [right base branch for your changes](https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md#branch-conventions), then:`, 166 `- If the changes should go to the \`${best.branch}\` branch, [change the base branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-base-branch-of-a-pull-request).`, 167 `- If the changes should go to the \`${current.branch}\` branch, rebase your PR onto the correct merge-base:`, 168 ' ```bash', 169 ` # git rebase --onto $(git merge-base upstream/${current.branch} HEAD) $(git merge-base upstream/${best.branch} HEAD)`, 170 ` git rebase --onto ${current.sha} ${best.sha}`, 171 ` git push --force-with-lease`, 172 ' ```', 173 ].join('\n') 174 175 await postReview({ github, context, core, dry, body, reviewKey }) 176 177 throw new Error(`The PR contains commits from a different base.`) 178 } 179 } 180 181 await dismissReviews({ github, context, core, dry, reviewKey }) 182 183 let mergedSha, targetSha 184 185 if (prInfo.mergeable) { 186 core.info('The PR can be merged.') 187 188 mergedSha = prInfo.merge_commit_sha 189 targetSha = ( 190 await github.rest.repos.getCommit({ 191 ...context.repo, 192 ref: prInfo.merge_commit_sha, 193 }) 194 ).data.parents[0].sha 195 } else { 196 core.warning('The PR has a merge conflict.') 197 198 mergedSha = head.sha 199 targetSha = ( 200 await github.rest.repos.compareCommitsWithBasehead({ 201 ...context.repo, 202 basehead: `${base.sha}...${head.sha}`, 203 }) 204 ).data.merge_base_commit.sha 205 } 206 207 core.info( 208 `Checking the commits:\nmerged: ${mergedSha}\ntarget: ${targetSha}`, 209 ) 210 core.setOutput('mergedSha', mergedSha) 211 core.setOutput('targetSha', targetSha) 212 213 const systems = await supportedSystems({ github, context, targetSha }) 214 core.setOutput('systems', systems) 215 216 const files = ( 217 await github.paginate(github.rest.pulls.listFiles, { 218 ...context.repo, 219 pull_number: context.payload.pull_request.number, 220 per_page: 100, 221 }) 222 ).map((file) => file.filename) 223 224 const touched = [] 225 if (files.includes('ci/pinned.json')) touched.push('pinned') 226 core.setOutput('touched', touched) 227 228 return 229 } 230 throw new Error( 231 "Not retrying anymore. It's likely that GitHub is having internal issues: check https://www.githubstatus.com.", 232 ) 233}