workflows/get-merge-commit: support merge conflicts, run as step in main jobs (#410430)

This commit is contained in:
Jörg Thalheim 2025-05-24 12:25:02 +02:00 committed by GitHub
commit b29abce2e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 173 additions and 218 deletions

View file

@ -0,0 +1,66 @@
name: Get merge commit
description: 'Checks whether the Pull Request is mergeable and returns two commit hashes: The result of a temporary merge of the head branch into the target branch ("merged"), and the parent of that commit on the target branch ("target"). Handles push events and merge conflicts gracefully.'
outputs:
mergedSha:
description: "The merge commit SHA"
value: ${{ fromJSON(steps.merged.outputs.result).mergedSha }}
targetSha:
description: "The target commit SHA"
value: ${{ fromJSON(steps.merged.outputs.result).targetSha }}
runs:
using: composite
steps:
- id: merged
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
if (context.eventName == 'push') return { mergedSha: context.sha }
for (const retryInterval of [5, 10, 20, 40, 80]) {
console.log("Checking whether the pull request can be merged...")
const prInfo = (await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number
})).data
if (prInfo.state != 'open') throw new Error ("PR is not open anymore.")
if (prInfo.mergeable == null) {
console.log(`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
}
if (prInfo.mergeable) {
console.log("The PR can be merged.")
const mergedSha = prInfo.merge_commit_sha
const targetSha = (await github.rest.repos.getCommit({
owner: context.repo.owner,
repo: context.repo.repo,
ref: prInfo.merge_commit_sha
})).data.parents[0].sha
console.log(`Checking the commits:\nmerged:${mergedSha}\ntarget:${targetSha}`)
return { mergedSha, targetSha }
} else {
console.log("The PR has a merge conflict.")
const mergedSha = prInfo.head.sha
const targetSha = (await github.rest.repos.compareCommitsWithBasehead({
owner: context.repo.owner,
repo: context.repo.repo,
basehead: `${prInfo.base.sha}...${prInfo.head.sha}`
})).data.merge_base_commit.sha
console.log(`Checking the commits:\nmerged:${mergedSha}\ntarget:${targetSha}`)
return { mergedSha, targetSha }
}
}
throw new Error("Not retrying anymore. It's likely that GitHub is having internal issues: check https://www.githubstatus.com.")

View file

@ -9,18 +9,20 @@ on:
permissions: {}
jobs:
get-merge-commit:
uses: ./.github/workflows/get-merge-commit.yml
nixos:
name: fmt-check
runs-on: ubuntu-24.04-arm
needs: get-merge-commit
if: needs.get-merge-commit.outputs.mergedSha
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
sparse-checkout: .github/actions
- name: Check if the PR can be merged and get the test merge commit
uses: ./.github/actions/get-merge-commit
id: get-merge-commit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ steps.get-merge-commit.outputs.mergedSha }}
- uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
with:

View file

@ -32,7 +32,14 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
sparse-checkout: .github/actions
- name: Check if the PR can be merged and get the test merge commit
uses: ./.github/actions/get-merge-commit
id: get-merge-commit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ steps.get-merge-commit.outputs.mergedSha }}
- uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31

View file

@ -37,17 +37,19 @@ env:
DRY_MODE: ${{ github.event.pull_request.draft && '1' || '' }}
jobs:
get-merge-commit:
if: github.repository_owner == 'NixOS'
uses: ./.github/workflows/get-merge-commit.yml
# Check that code owners is valid
check:
name: Check
runs-on: ubuntu-24.04-arm
needs: get-merge-commit
if: github.repository_owner == 'NixOS' && needs.get-merge-commit.outputs.mergedSha
if: github.repository_owner == 'NixOS'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
sparse-checkout: .github/actions
- name: Check if the PR can be merged and get the test merge commit
uses: ./.github/actions/get-merge-commit
id: get-merge-commit
- uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -77,7 +79,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
ref: ${{ steps.get-merge-commit.outputs.mergedSha }}
path: pr
- name: Validate codeowners

View file

@ -9,18 +9,21 @@ on:
permissions: {}
jobs:
get-merge-commit:
uses: ./.github/workflows/get-merge-commit.yml
eval-aliases:
name: Eval nixpkgs with aliases enabled
runs-on: ubuntu-24.04-arm
needs: [ get-merge-commit ]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
sparse-checkout: .github/actions
- name: Check if the PR can be merged and get the test merge commit
uses: ./.github/actions/get-merge-commit
id: get-merge-commit
- name: Check out the PR at the test merge commit
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
ref: ${{ steps.get-merge-commit.outputs.mergedSha }}
path: nixpkgs
- name: Install Nix

View file

@ -19,17 +19,36 @@ on:
permissions: {}
jobs:
get-merge-commit:
uses: ./.github/workflows/get-merge-commit.yml
prepare:
name: Prepare
runs-on: ubuntu-24.04-arm
outputs:
mergedSha: ${{ steps.get-merge-commit.outputs.mergedSha }}
targetSha: ${{ steps.get-merge-commit.outputs.targetSha }}
systems: ${{ steps.systems.outputs.systems }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
sparse-checkout: |
.github/actions
ci/supportedSystems.json
- name: Check if the PR can be merged and get the test merge commit
uses: ./.github/actions/get-merge-commit
id: get-merge-commit
- name: Load supported systems
id: systems
run: |
echo "systems=$(jq -c <ci/supportedSystems.json)" >> "$GITHUB_OUTPUT"
outpaths:
name: Outpaths
runs-on: ubuntu-24.04-arm
needs: [ get-merge-commit ]
needs: [ prepare ]
strategy:
fail-fast: false
matrix:
system: ${{ fromJSON(needs.get-merge-commit.outputs.systems) }}
system: ${{ fromJSON(needs.prepare.outputs.systems) }}
steps:
- name: Enable swap
run: |
@ -41,7 +60,7 @@ jobs:
- name: Check out the PR at the test merge commit
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
ref: ${{ needs.prepare.outputs.mergedSha }}
path: nixpkgs
- name: Install Nix
@ -67,7 +86,7 @@ jobs:
process:
name: Process
runs-on: ubuntu-24.04-arm
needs: [ outpaths, get-merge-commit ]
needs: [ prepare, outpaths ]
outputs:
targetRunId: ${{ steps.targetRunId.outputs.targetRunId }}
steps:
@ -80,7 +99,7 @@ jobs:
- name: Check out the PR at the test merge commit
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
ref: ${{ needs.prepare.outputs.mergedSha }}
fetch-depth: 2
path: nixpkgs
@ -102,7 +121,7 @@ jobs:
path: prResult/*
- name: Get target run id
if: needs.get-merge-commit.outputs.targetSha
if: needs.prepare.outputs.targetSha
id: targetRunId
run: |
# Get the latest eval.yml workflow run for the PR's target commit
@ -131,7 +150,7 @@ jobs:
echo "targetRunId=$runId" >> "$GITHUB_OUTPUT"
env:
REPOSITORY: ${{ github.repository }}
TARGET_SHA: ${{ needs.get-merge-commit.outputs.targetSha }}
TARGET_SHA: ${{ needs.prepare.outputs.targetSha }}
GH_TOKEN: ${{ github.token }}
- uses: actions/download-artifact@v4
@ -145,8 +164,8 @@ jobs:
- name: Compare against the target branch
if: steps.targetRunId.outputs.targetRunId
run: |
git -C nixpkgs worktree add ../target ${{ needs.get-merge-commit.outputs.targetSha }}
git -C nixpkgs diff --name-only ${{ needs.get-merge-commit.outputs.targetSha }} \
git -C nixpkgs worktree add ../target ${{ needs.prepare.outputs.targetSha }}
git -C nixpkgs diff --name-only ${{ needs.prepare.outputs.targetSha }} \
| jq --raw-input --slurp 'split("\n")[:-1]' > touched-files.json
# Use the target branch to get accurate maintainer info
@ -172,7 +191,7 @@ jobs:
tag:
name: Tag
runs-on: ubuntu-24.04-arm
needs: [ get-merge-commit, process ]
needs: [ prepare, process ]
if: needs.process.outputs.targetRunId
permissions:
pull-requests: write
@ -204,7 +223,7 @@ jobs:
- name: Check out Nixpkgs at the base commit
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ needs.get-merge-commit.outputs.targetSha }}
ref: ${{ needs.prepare.outputs.targetSha }}
path: base
sparse-checkout: ci

View file

@ -1,58 +0,0 @@
name: Get merge commit
on:
pull_request:
paths:
- .github/workflows/get-merge-commit.yml
workflow_call:
outputs:
mergedSha:
description: "The merge commit SHA"
value: ${{ jobs.resolve-merge-commit.outputs.mergedSha }}
targetSha:
description: "The target commit SHA"
value: ${{ jobs.resolve-merge-commit.outputs.targetSha }}
systems:
description: "The supported systems"
value: ${{ jobs.resolve-merge-commit.outputs.systems }}
permissions: {}
jobs:
resolve-merge-commit:
runs-on: ubuntu-24.04-arm
outputs:
mergedSha: ${{ steps.merged.outputs.mergedSha }}
targetSha: ${{ steps.merged.outputs.targetSha }}
systems: ${{ steps.systems.outputs.systems }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
path: base
sparse-checkout: ci
- name: Check if the PR can be merged and get the test merge commit
id: merged
env:
GH_TOKEN: ${{ github.token }}
GH_EVENT: ${{ github.event_name }}
run: |
case "$GH_EVENT" in
push)
echo "mergedSha=${{ github.sha }}" >> "$GITHUB_OUTPUT"
;;
pull_request*)
if commits=$(base/ci/get-merge-commit.sh ${{ github.repository }} ${{ github.event.number }}); then
echo -e "Checking the commits:\n$commits"
echo "$commits" >> "$GITHUB_OUTPUT"
else
# Skipping so that no notifications are sent
echo "Skipping the rest..."
fi
;;
esac
- name: Load supported systems
id: systems
run: |
echo "systems=$(jq -c <base/ci/supportedSystems.json)" >> "$GITHUB_OUTPUT"

View file

@ -12,18 +12,20 @@ on:
permissions: {}
jobs:
get-merge-commit:
uses: ./.github/workflows/get-merge-commit.yml
nixpkgs-lib-tests:
name: nixpkgs-lib-tests
runs-on: ubuntu-24.04
needs: get-merge-commit
if: needs.get-merge-commit.outputs.mergedSha
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
sparse-checkout: .github/actions
- name: Check if the PR can be merged and get the test merge commit
uses: ./.github/actions/get-merge-commit
id: get-merge-commit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ steps.get-merge-commit.outputs.mergedSha }}
- uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
with:

View file

@ -34,7 +34,14 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
sparse-checkout: .github/actions
- name: Check if the PR can be merged and get the test merge commit
uses: ./.github/actions/get-merge-commit
id: get-merge-commit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ steps.get-merge-commit.outputs.mergedSha }}
- uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
with:

View file

@ -21,7 +21,14 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
sparse-checkout: .github/actions
- name: Check if the PR can be merged and get the test merge commit
uses: ./.github/actions/get-merge-commit
id: get-merge-commit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ steps.get-merge-commit.outputs.mergedSha }}
- uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
with:

View file

@ -9,18 +9,21 @@ on:
permissions: {}
jobs:
get-merge-commit:
uses: ./.github/workflows/get-merge-commit.yml
tests:
name: nix-files-parseable-check
runs-on: ubuntu-24.04-arm
needs: get-merge-commit
if: "needs.get-merge-commit.outputs.mergedSha && !contains(github.event.pull_request.title, '[skip treewide]')"
if: "!contains(github.event.pull_request.title, '[skip treewide]')"
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
sparse-checkout: .github/actions
- name: Check if the PR can be merged and get the test merge commit
uses: ./.github/actions/get-merge-commit
id: get-merge-commit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ steps.get-merge-commit.outputs.mergedSha }}
- uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
with:

View file

@ -17,21 +17,23 @@ permissions: {}
# There is a feature request for suppressing notifications on concurrency-canceled runs: https://github.com/orgs/community/discussions/13015
jobs:
get-merge-commit:
uses: ./.github/workflows/get-merge-commit.yml
check:
name: nixpkgs-vet
# This needs to be x86_64-linux, because we depend on the tooling being pre-built in the GitHub releases.
runs-on: ubuntu-24.04
# This should take 1 minute at most, but let's be generous. The default of 6 hours is definitely too long.
timeout-minutes: 10
needs: get-merge-commit
if: needs.get-merge-commit.outputs.mergedSha
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
sparse-checkout: .github/actions
- name: Check if the PR can be merged and get the test merge commit
uses: ./.github/actions/get-merge-commit
id: get-merge-commit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ steps.get-merge-commit.outputs.mergedSha }}
# Fetches the merge commit and its parents
fetch-depth: 2

View file

@ -15,6 +15,7 @@
# CI
/.github/*_TEMPLATE* @SigmaSquadron
/.github/actions @NixOS/Security @Mic92 @zowoq @infinisil @azuwis @wolfgangwalther
/.github/workflows @NixOS/Security @Mic92 @zowoq @infinisil @azuwis @wolfgangwalther
/.github/workflows/check-format.yml @infinisil @wolfgangwalther
/.github/workflows/codeowners-v2.yml @infinisil @wolfgangwalther

View file

@ -40,46 +40,3 @@ Why not just build the tooling right from the PRs Nixpkgs version?
- Because it makes the CI check very fast, since no Nix builds need to be done, even for mass rebuilds.
- Because it improves security, since we don't have to build potentially untrusted code from PRs.
The tool only needs a very minimal Nix evaluation at runtime, which can work with [readonly-mode](https://nixos.org/manual/nix/stable/command-ref/opt-common.html#opt-readonly-mode) and [restrict-eval](https://nixos.org/manual/nix/stable/command-ref/conf-file.html#conf-restrict-eval).
## `get-merge-commit.sh GITHUB_REPO PR_NUMBER`
Check whether a PR is mergeable and return the test merge commit as
[computed by GitHub](https://docs.github.com/en/rest/guides/using-the-rest-api-to-interact-with-your-git-database?apiVersion=2022-11-28#checking-mergeability-of-pull-requests) and its parent.
Arguments:
- `GITHUB_REPO`: The repository of the PR, e.g. `NixOS/nixpkgs`
- `PR_NUMBER`: The PR number, e.g. `1234`
Exit codes:
- 0: The PR can be merged, the hashes of the test merge commit and the target commit are returned on stdout
- 1: The PR cannot be merged because it's not open anymore
- 2: The PR cannot be merged because it has a merge conflict
- 3: The merge commit isn't being computed, GitHub is likely having internal issues, unknown if the PR is mergeable
### Usage
This script is implemented as a reusable GitHub Actions workflow, and can be used as follows:
```yaml
on: pull_request_target
# We need a token to query the API, but it doesn't need any special permissions
permissions: {}
jobs:
get-merge-commit:
# use the relative path of the get-merge-commit workflow yaml here
uses: ./.github/workflows/get-merge-commit.yml
build:
name: Build
runs-on: ubuntu-24.04
needs: get-merge-commit
steps:
- uses: actions/checkout@<VERSION>
# Add this to _all_ subsequent steps to skip them
if: needs.get-merge-commit.outputs.mergedSha
with:
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
- ...
```

View file

@ -1,65 +0,0 @@
#!/usr/bin/env bash
# See ./README.md for docs
set -euo pipefail
log() {
echo "$@" >&2
}
if (( $# < 2 )); then
log "Usage: $0 GITHUB_REPO PR_NUMBER"
exit 99
fi
repo=$1
prNumber=$2
# Retry the API query this many times
retryCount=5
# Start with 5 seconds, but double every retry
retryInterval=5
while true; do
log "Checking whether the pull request can be merged"
prInfo=$(gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/$repo/pulls/$prNumber")
# Non-open PRs won't have their mergeability computed no matter what
state=$(jq -r .state <<< "$prInfo")
if [[ "$state" != open ]]; then
log "PR is not open anymore"
exit 1
fi
mergeable=$(jq -r .mergeable <<< "$prInfo")
if [[ "$mergeable" == "null" ]]; then
if (( retryCount == 0 )); then
log "Not retrying anymore. It's likely that GitHub is having internal issues: check https://www.githubstatus.com/"
exit 3
else
(( retryCount -= 1 )) || true
# null indicates that GitHub is still computing whether it's mergeable
# Wait a couple seconds before trying again
log "GitHub is still computing whether this PR can be merged, waiting $retryInterval seconds before trying again ($retryCount retries left)"
sleep "$retryInterval"
(( retryInterval *= 2 )) || true
fi
else
break
fi
done
if [[ "$mergeable" == "true" ]]; then
log "The PR can be merged"
mergedSha="$(jq -r .merge_commit_sha <<< "$prInfo")"
echo "mergedSha=$mergedSha"
targetSha="$(gh api "/repos/$repo/commits/$mergedSha" --jq '.parents[0].sha')"
echo "targetSha=$targetSha"
else
log "The PR has a merge conflict"
exit 2
fi