mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-11-10 00:43:47 +01:00
This removes the "owners" check from codeowners-validator. With it, all tokens and permissions can be removed, because these were only needed to make these requests. This solves the problem of codeowners-validator not supporting our new nested team structure for nixpkgs-maintainers. To make the onboarding of new teams easier, we moved all teams "under" the nixpkgs-maintainers team. This makes them inherit the right privileges (triage) for Nixpkgs. However, this inheritance is not recognized by codeowners-validator, thus it assumes that these teams don't have access to Nixpkgs. This then fails the owners check immediately. Removing the owners check also has a few other advantages: - This check depends on external state: If a user is renamed or a team removed, the check will fail. This makes it a bad check for required status checks or merge queues - the check might fail randomly, independent of the current PR. - Running this check in a fork will never work, because the respective users and teams don't have access to the fork's repo. Both of this required us to set `continue-on-error: true` most of the time.
232 lines
9.3 KiB
JavaScript
232 lines
9.3 KiB
JavaScript
const { classify } = require('../supportedBranches.js')
|
|
const { postReview } = require('./reviews.js')
|
|
|
|
module.exports = async ({ github, context, core, dry }) => {
|
|
const pull_number = context.payload.pull_request.number
|
|
|
|
for (const retryInterval of [5, 10, 20, 40, 80]) {
|
|
core.info('Checking whether the pull request can be merged...')
|
|
const prInfo = (
|
|
await github.rest.pulls.get({
|
|
...context.repo,
|
|
pull_number,
|
|
})
|
|
).data
|
|
|
|
if (prInfo.state !== 'open') throw new Error('PR is not open anymore.')
|
|
|
|
if (prInfo.mergeable == null) {
|
|
core.info(
|
|
`GitHub is still computing whether this PR can be merged, waiting ${retryInterval} seconds before trying again...`,
|
|
)
|
|
await new Promise((resolve) => setTimeout(resolve, retryInterval * 1000))
|
|
continue
|
|
}
|
|
|
|
const { base, head } = prInfo
|
|
|
|
const baseClassification = classify(base.ref)
|
|
core.setOutput('base', baseClassification)
|
|
console.log('base classification:', baseClassification)
|
|
|
|
const headClassification =
|
|
base.repo.full_name === head.repo.full_name
|
|
? classify(head.ref)
|
|
: // PRs from forks are always considered WIP.
|
|
{ type: ['wip'] }
|
|
core.setOutput('head', headClassification)
|
|
console.log('head classification:', headClassification)
|
|
|
|
if (baseClassification.type.includes('channel')) {
|
|
const { stable, version } = baseClassification
|
|
const correctBranch = stable ? `release-${version}` : 'master'
|
|
const body = [
|
|
'The `nixos-*` and `nixpkgs-*` branches are pushed to by the channel release script and should not be merged into directly.',
|
|
'',
|
|
`Please target \`${correctBranch}\` instead.`,
|
|
].join('\n')
|
|
|
|
await postReview({ github, context, core, dry, body })
|
|
|
|
throw new Error('The PR targets a channel branch.')
|
|
}
|
|
|
|
if (headClassification.type.includes('wip')) {
|
|
// In the following, we look at the git history to determine the base branch that
|
|
// this Pull Request branched off of. This is *supposed* to be the branch that it
|
|
// merges into, but humans make mistakes. Once that happens we want to error out as
|
|
// early as possible.
|
|
|
|
// To determine the "real base", we are looking at the merge-base of primary development
|
|
// branches and the head of the PR. The merge-base which results in the least number of
|
|
// commits between that base and head is the real base. We can query for this via GitHub's
|
|
// REST API. There can be multiple candidates for the real base with the same number of
|
|
// commits. In this case we pick the "best" candidate by a fixed ordering of branches,
|
|
// as defined in ci/supportedBranches.js.
|
|
//
|
|
// These requests take a while, when comparing against the wrong release - they need
|
|
// to look at way more than 10k commits in that case. Thus, we try to minimize the
|
|
// number of requests across releases:
|
|
// - First, we look at the primary development branches only: master and release-xx.yy.
|
|
// The branch with the fewest commits gives us the release this PR belongs to.
|
|
// - We then compare this number against the relevant staging branches for this release
|
|
// to find the exact branch that this belongs to.
|
|
|
|
// All potential development branches
|
|
const branches = (
|
|
await github.paginate(github.rest.repos.listBranches, {
|
|
...context.repo,
|
|
per_page: 100,
|
|
})
|
|
).map(({ name }) => classify(name))
|
|
|
|
// All stable primary development branches from latest to oldest.
|
|
const releases = branches
|
|
.filter(({ stable, type }) => type.includes('primary') && stable)
|
|
.sort((a, b) => b.version.localeCompare(a.version))
|
|
|
|
async function mergeBase({ branch, order, version }) {
|
|
const { data } = await github.rest.repos.compareCommitsWithBasehead({
|
|
...context.repo,
|
|
basehead: `${branch}...${head.sha}`,
|
|
// Pagination for this endpoint is about the commits listed, which we don't care about.
|
|
per_page: 1,
|
|
// Taking the second page skips the list of files of this changeset.
|
|
page: 2,
|
|
})
|
|
return {
|
|
branch,
|
|
order,
|
|
version,
|
|
commits: data.total_commits,
|
|
sha: data.merge_base_commit.sha,
|
|
}
|
|
}
|
|
|
|
// Multiple branches can be OK at the same time, if the PR was created of a merge-base,
|
|
// thus storing as array.
|
|
let candidates = [await mergeBase(classify('master'))]
|
|
for (const release of releases) {
|
|
const nextCandidate = await mergeBase(release)
|
|
if (candidates[0].commits === nextCandidate.commits)
|
|
candidates.push(nextCandidate)
|
|
if (candidates[0].commits > nextCandidate.commits)
|
|
candidates = [nextCandidate]
|
|
// The number 10000 is principally arbitrary, but the GitHub API returns this value
|
|
// when the number of commits exceeds it in reality. The difference between two stable releases
|
|
// is certainly more than 10k commits, thus this works for us as well: If we're targeting
|
|
// a wrong release, the number *will* be 10000.
|
|
if (candidates[0].commits < 10000) break
|
|
}
|
|
|
|
core.info(`This PR is for NixOS ${candidates[0].version}.`)
|
|
|
|
// Secondary development branches for the selected version only.
|
|
const secondary = branches.filter(
|
|
({ branch, type, version }) =>
|
|
type.includes('secondary') && version === candidates[0].version,
|
|
)
|
|
|
|
// Make sure that we always check the current target as well, even if its a WIP branch.
|
|
// If it's not a WIP branch, it was already included in either releases or secondary.
|
|
if (classify(base.ref).type.includes('wip')) {
|
|
secondary.push(classify(base.ref))
|
|
}
|
|
|
|
for (const branch of secondary) {
|
|
const nextCandidate = await mergeBase(branch)
|
|
if (candidates[0].commits === nextCandidate.commits)
|
|
candidates.push(nextCandidate)
|
|
if (candidates[0].commits > nextCandidate.commits)
|
|
candidates = [nextCandidate]
|
|
}
|
|
|
|
// If the current branch is among the candidates, this is always better than any other,
|
|
// thus sorting at -1.
|
|
candidates = candidates
|
|
.map((candidate) =>
|
|
candidate.branch === base.ref
|
|
? { ...candidate, order: -1 }
|
|
: candidate,
|
|
)
|
|
.sort((a, b) => a.order - b.order)
|
|
|
|
const best = candidates.at(0)
|
|
|
|
core.info('The base branches for this PR are:')
|
|
core.info(`github: ${base.ref}`)
|
|
core.info(
|
|
`candidates: ${candidates.map(({ branch }) => branch).join(',')}`,
|
|
)
|
|
core.info(`best candidate: ${best.branch}`)
|
|
|
|
if (best.branch !== base.ref) {
|
|
const current = await mergeBase(classify(base.ref))
|
|
const body = [
|
|
`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:`,
|
|
`- 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).`,
|
|
`- If the changes should go to the \`${current.branch}\` branch, rebase your PR onto the correct merge-base:`,
|
|
' ```bash',
|
|
` # git rebase --onto $(git merge-base upstream/${current.branch} HEAD) $(git merge-base upstream/${best.branch} HEAD)`,
|
|
` git rebase --onto ${current.sha} ${best.sha}`,
|
|
` git push --force-with-lease`,
|
|
' ```',
|
|
].join('\n')
|
|
|
|
await postReview({ github, context, core, dry, body })
|
|
|
|
throw new Error(`The PR contains commits from a different base.`)
|
|
}
|
|
}
|
|
|
|
let mergedSha, targetSha
|
|
|
|
if (prInfo.mergeable) {
|
|
core.info('The PR can be merged.')
|
|
|
|
mergedSha = prInfo.merge_commit_sha
|
|
targetSha = (
|
|
await github.rest.repos.getCommit({
|
|
...context.repo,
|
|
ref: prInfo.merge_commit_sha,
|
|
})
|
|
).data.parents[0].sha
|
|
} else {
|
|
core.warning('The PR has a merge conflict.')
|
|
|
|
mergedSha = head.sha
|
|
targetSha = (
|
|
await github.rest.repos.compareCommitsWithBasehead({
|
|
...context.repo,
|
|
basehead: `${base.sha}...${head.sha}`,
|
|
})
|
|
).data.merge_base_commit.sha
|
|
}
|
|
|
|
core.info(
|
|
`Checking the commits:\nmerged: ${mergedSha}\ntarget: ${targetSha}`,
|
|
)
|
|
core.setOutput('mergedSha', mergedSha)
|
|
core.setOutput('targetSha', targetSha)
|
|
|
|
core.setOutput('systems', require('../supportedSystems.json'))
|
|
|
|
const files = (
|
|
await github.paginate(github.rest.pulls.listFiles, {
|
|
...context.repo,
|
|
pull_number: context.payload.pull_request.number,
|
|
per_page: 100,
|
|
})
|
|
).map((file) => file.filename)
|
|
|
|
const touched = []
|
|
if (files.includes('ci/pinned.json')) touched.push('pinned')
|
|
core.setOutput('touched', touched)
|
|
|
|
return
|
|
}
|
|
throw new Error(
|
|
"Not retrying anymore. It's likely that GitHub is having internal issues: check https://www.githubstatus.com.",
|
|
)
|
|
}
|