From b88d1d72f7d5856f582caa53dbacadbacd5e25f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:07:55 -0300 Subject: [PATCH] ci: automate conflict-free backport merges --- .github/workflows/backport.yml | 74 +++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index e7f1fcd95..5fc250718 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -4,7 +4,7 @@ on: types: [closed, labeled] permissions: - contents: write + contents: read pull-requests: write jobs: @@ -18,6 +18,78 @@ jobs: startsWith(github.event.label.name, 'backport ') ) steps: + - name: Generate backport App token + id: backport-app + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.XCAT_BACKPORT_APP_CLIENT_ID }} + private-key: ${{ secrets.XCAT_BACKPORT_APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + permission-workflows: write - uses: actions/checkout@v6 + with: + token: ${{ steps.backport-app.outputs.token }} - name: Create backport pull requests + id: backport uses: korthout/backport-action@v3 + with: + github_token: ${{ steps.backport-app.outputs.token }} + auto_merge_enabled: true + - name: Approve backport pull requests + if: steps.backport.outputs.created_pull_numbers != '' + uses: actions/github-script@v8 + env: + BACKPORT_APP_SLUG: ${{ steps.backport-app.outputs.app-slug }} + CREATED_PULL_NUMBERS: ${{ steps.backport.outputs.created_pull_numbers }} + with: + github-token: ${{ github.token }} + script: | + const expectedAuthor = `${process.env.BACKPORT_APP_SLUG}[bot]`; + const pullNumbers = process.env.CREATED_PULL_NUMBERS + .trim() + .split(/\s+/) + .map(Number); + + for (const pull_number of pullNumbers) { + if (!Number.isSafeInteger(pull_number) || pull_number <= 0) { + throw new Error(`Invalid backport pull request number: ${pull_number}`); + } + + const { data: pull } = await github.rest.pulls.get({ + ...context.repo, + pull_number, + }); + + if (pull.user?.login !== expectedAuthor) { + throw new Error( + `Refusing to approve #${pull_number}: expected author ${expectedAuthor}`, + ); + } + if (pull.head.repo?.full_name !== `${context.repo.owner}/${context.repo.repo}`) { + throw new Error( + `Refusing to approve #${pull_number}: head repository does not match`, + ); + } + if (pull.state !== 'open' || pull.draft) { + throw new Error( + `Refusing to approve #${pull_number}: pull request is not open and ready`, + ); + } + if (pull.auto_merge === null || pull.auto_merge === undefined) { + throw new Error( + `Refusing to approve #${pull_number}: auto-merge is not enabled`, + ); + } + + await github.rest.pulls.createReview({ + ...context.repo, + pull_number, + commit_id: pull.head.sha, + event: 'APPROVE', + body: 'Automated approval for a conflict-free backport of an already merged pull request.', + }); + } + - name: Fail when a backport needs intervention + if: steps.backport.outputs.was_successful != 'true' + run: exit 1