diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c107c340209..85af5ffd2a6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: inputs: pr: - description: Pull request number to build + description: Pull request numbers to build together, separated by spaces or commas (up to 8) required: true type: string linux: @@ -77,38 +77,47 @@ jobs: throw new Error('Only collaborators with write access can start or rerun builds'); } } - if (!/^[1-9][0-9]*$/.test(process.env.PR_NUMBER)) { - throw new Error('Enter a positive pull request number'); + if (!/^[1-9][0-9]*([ ,]+[1-9][0-9]*){0,7}$/.test(process.env.PR_NUMBER.trim())) { + throw new Error('Enter up to 8 positive pull request numbers separated by spaces or commas'); } } - const pull_number = manual ? Number(process.env.PR_NUMBER) : context.payload.pull_request.number; - if (!Number.isSafeInteger(pull_number) || pull_number < 1) { + const numbers = manual + ? [...new Set(process.env.PR_NUMBER.trim().split(/[ ,]+/).map(Number))] + : [context.payload.pull_request.number]; + if (numbers.some((pull_number) => !Number.isSafeInteger(pull_number) || pull_number < 1)) { throw new Error('Invalid pull request number'); } - let { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number }); - for (let attempt = 0; pr.mergeable === null && attempt < 10; ++attempt) { - await new Promise((resolve) => setTimeout(resolve, 3000)); - ({ data: pr } = await github.rest.pulls.get({ owner, repo, pull_number })); - } - if (pr.state !== 'open' || pr.base.ref !== 'main') { - throw new Error('The pull request must be open and target main'); - } - if (!manual && pr.head.sha !== context.payload.pull_request.head.sha) { - throw new Error('The pull request has newer commits; use the latest run'); - } - if (pr.mergeable === false) { - throw new Error('The pull request does not merge cleanly into main; rebase it first'); - } + const pulls = []; + const paths = []; + let truncated = false; let relinker = manual; - if (!manual) { - const { data: commits } = await github.rest.repos.listCommits({ - owner, repo, sha: 'main', author: pr.user.login, per_page: 1 - }); - relinker = commits.length > 0; + for (const pull_number of numbers) { + let { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number }); + for (let attempt = 0; pr.mergeable === null && attempt < 10; ++attempt) { + await new Promise((resolve) => setTimeout(resolve, 3000)); + ({ data: pr } = await github.rest.pulls.get({ owner, repo, pull_number })); + } + if (pr.state !== 'open' || pr.base.ref !== 'main') { + throw new Error(`Pull request #${pull_number} must be open and target main`); + } + if (!manual && pr.head.sha !== context.payload.pull_request.head.sha) { + throw new Error('The pull request has newer commits; use the latest run'); + } + if (pr.mergeable === false) { + throw new Error(`Pull request #${pull_number} does not merge cleanly into main; rebase it first`); + } + if (!manual) { + const { data: commits } = await github.rest.repos.listCommits({ + owner, repo, sha: 'main', author: pr.user.login, per_page: 1 + }); + relinker = commits.length > 0; + } + const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 }); + truncated ||= files.length >= 3000; + paths.push(...files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean))); + pulls.push({ number: pull_number, head: pr.head.sha }); } - const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 }); - const paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean)); - const touches = (...prefixes) => files.length >= 3000 || + const touches = (...prefixes) => truncated || paths.some((path) => prefixes.some((prefix) => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix)); const workflow = '.github/workflows/build.yml'; const libc = 'core/libs/prx/libc/'; @@ -121,15 +130,16 @@ jobs: const writeWatch = touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc, workflow); const { data: main } = await github.rest.repos.getBranch({ owner, repo, branch: 'main' }); core.setOutput('sha', main.commit.sha); - core.setOutput('pr', String(pull_number)); - core.setOutput('head', pr.head.sha); + core.setOutput('pr', pulls.map((pull) => pull.number).join(' ')); + core.setOutput('head', pulls.map((pull) => pull.head).join(' ')); core.setOutput('relinker', relinker); core.setOutput('register-context', registerContext); core.setOutput('write-watch', writeWatch); core.setOutput('full', manual); - core.info(`PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}`); + const merged = pulls.map((pull) => `PR #${pull.number} at ${pull.head} merged into main at ${main.commit.sha}`); + merged.forEach((line) => core.info(line)); await core.summary.addRaw( - `PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}: full builds ${manual ? 'enabled' : 'disabled'}, ` + + `${merged.join(', then ')}: full builds ${manual ? 'enabled' : 'disabled'}, ` + `relinker ${relinker ? 'enabled' : 'skipped: no relinker file changed or the author has no commit in main'}, ` + `register context tests ${registerContext ? 'enabled' : 'skipped'}, write watch tests ${writeWatch ? 'enabled' : 'skipped'}` ).write(); @@ -167,9 +177,15 @@ jobs: PR_NUMBER: ${{ needs.eligibility.outputs.pr }} PR_HEAD: ${{ needs.eligibility.outputs.head }} run: | - git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head" - if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi - if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi + read -ra numbers <<< "$PR_NUMBER" + read -ra heads <<< "$PR_HEAD" + refspecs=() + for number in "${numbers[@]}"; do refspecs+=("refs/pull/$number/head:refs/remotes/pull/$number"); done + git fetch --no-tags --unshallow origin "${refspecs[@]}" + for i in "${!numbers[@]}"; do + if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi + if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi + done - if: runner.os == 'Windows' id: toolchain uses: actions/cache@v6 @@ -235,9 +251,15 @@ jobs: PR_NUMBER: ${{ needs.eligibility.outputs.pr }} PR_HEAD: ${{ needs.eligibility.outputs.head }} run: | - git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head" - if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi - if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi + read -ra numbers <<< "$PR_NUMBER" + read -ra heads <<< "$PR_HEAD" + refspecs=() + for number in "${numbers[@]}"; do refspecs+=("refs/pull/$number/head:refs/remotes/pull/$number"); done + git fetch --no-tags --unshallow origin "${refspecs[@]}" + for i in "${!numbers[@]}"; do + if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi + if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi + done git submodule update --init --recursive - uses: hendrikmuhs/ccache-action@v1.2.24 with: @@ -305,9 +327,15 @@ jobs: PR_NUMBER: ${{ needs.eligibility.outputs.pr }} PR_HEAD: ${{ needs.eligibility.outputs.head }} run: | - git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head" - if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi - if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi + read -ra numbers <<< "$PR_NUMBER" + read -ra heads <<< "$PR_HEAD" + refspecs=() + for number in "${numbers[@]}"; do refspecs+=("refs/pull/$number/head:refs/remotes/pull/$number"); done + git fetch --no-tags --unshallow origin "${refspecs[@]}" + for i in "${!numbers[@]}"; do + if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi + if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi + done git submodule update --init --recursive - id: toolchain uses: actions/cache@v6