From 6c8a676e51fa64edf8f9cc17aa2c332a784c97ec Mon Sep 17 00:00:00 2001 From: Christian Schilling Date: Sun, 2 Nov 2025 10:12:43 +0100 Subject: [PATCH] Enforce single commit PRs --- .../workflows/pr-single-commit-up-to-date.yml | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 .github/workflows/pr-single-commit-up-to-date.yml diff --git a/.github/workflows/pr-single-commit-up-to-date.yml b/.github/workflows/pr-single-commit-up-to-date.yml new file mode 100644 index 000000000..8056d7628 --- /dev/null +++ b/.github/workflows/pr-single-commit-up-to-date.yml @@ -0,0 +1,75 @@ +# .github/workflows/pr-single-commit-up-to-date.yml +name: Enforce single commit & up-to-date + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] + pull_request_target: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] + +permissions: + contents: read + pull-requests: write + +jobs: + check-ahead-behind: + if: ${{ !github.event.pull_request.draft }} + runs-on: ubuntu-22.04 + steps: + - name: Checkout PR HEAD (full history) + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: Fetch base branch + run: | + git fetch origin ${{ github.event.pull_request.base.ref }} + + - name: Compute ahead/behind + id: ab + shell: bash + run: | + BASE_SHA=${{ github.event.pull_request.base.sha }} + read BEHIND AHEAD < <(git rev-list --left-right --count ${BASE_SHA}...HEAD) + echo "head=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + echo "base=${BASE_SHA}" >> $GITHUB_OUTPUT + echo "behind=$BEHIND" >> $GITHUB_OUTPUT + echo "ahead=$AHEAD" >> $GITHUB_OUTPUT + echo "Behind: $BEHIND, Ahead: $AHEAD" + + - name: Enforce rule + shell: bash + run: | + BASE=${{ steps.ab.outputs.base }} + HEAD=${{ steps.ab.outputs.head}} + BEHIND=${{ steps.ab.outputs.behind }} + AHEAD=${{ steps.ab.outputs.ahead }} + if [[ "$BEHIND" -ne 0 || "$AHEAD" -ne 1 ]]; then + echo "PR must be exactly 1 commit ahead and 0 behind the base branch." + echo "base=$BASE, HEAD=$HEAD" + echo "Ahead=$AHEAD, Behind=$BEHIND" + echo "Rebase and squash to fix." + exit 1 + fi + echo "OK: PR is exactly 1 ahead and 0 behind." + + - name: Convert PR to draft on failure (base repo context) + if: failure() + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + if (!pr) { + core.setFailed('No pull_request context available.'); + } else if (pr.draft) { + core.info('PR is already in draft.'); + } else { + await github.request('POST /repos/{owner}/{repo}/pulls/{pull_number}/convert-to-draft', { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + headers: { 'X-GitHub-Api-Version': '2022-11-28' } + }); + core.info(`Converted PR #${pr.number} to draft.`); + }