Publish Bottles and Merge PR #421
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish Bottles and Merge PR | |
| on: | |
| pull_request_target: | |
| types: | |
| - labeled | |
| workflow_run: | |
| workflows: | |
| - Build, Test and Upload Bottles | |
| types: | |
| - completed | |
| concurrency: | |
| group: publish-${{ github.event.pull_request.number || github.event.workflow_run.pull_requests[0].number || github.run_id }} | |
| cancel-in-progress: false | |
| defaults: | |
| run: | |
| shell: bash | |
| permissions: | |
| contents: read | |
| jobs: | |
| check: | |
| if: > | |
| (github.event_name == 'pull_request_target' && github.event.label.name == 'Automerge') || | |
| (github.event_name == 'workflow_run' && | |
| github.event.workflow_run.conclusion == 'success' && | |
| github.event.workflow_run.event == 'pull_request') | |
| runs-on: ubuntu-22.04 | |
| permissions: | |
| actions: read | |
| contents: read | |
| pull-requests: read | |
| outputs: | |
| publish: ${{ steps.check.outputs.publish }} | |
| pr: ${{ steps.check.outputs.pr }} | |
| head_ref: ${{ steps.check.outputs.head_ref }} | |
| head_repo_clone_url: ${{ steps.check.outputs.head_repo_clone_url }} | |
| steps: | |
| - name: Evaluate Publish Readiness | |
| id: check | |
| uses: actions/github-script@v8 | |
| with: | |
| github-token: ${{ github.token }} | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const defaultBranch = context.payload.repository.default_branch; | |
| const ciWorkflowName = 'Build, Test and Upload Bottles'; | |
| async function resolvePullRequestNumber() { | |
| if (context.eventName === 'pull_request_target') { | |
| return context.payload.pull_request.number; | |
| } | |
| const run = context.payload.workflow_run; | |
| let number = run.pull_requests?.[0]?.number; | |
| if (number) { | |
| return number; | |
| } | |
| const associated = await github.paginate( | |
| github.rest.repos.listPullRequestsAssociatedWithCommit, | |
| { | |
| owner, | |
| repo, | |
| commit_sha: run.head_sha, | |
| per_page: 100, | |
| }, | |
| ); | |
| const open = associated.filter((pr) => pr.state === 'open'); | |
| if (open.length !== 1) { | |
| core.notice(`Expected exactly one open PR for ${run.head_sha}; found ${open.length}.`); | |
| return null; | |
| } | |
| return open[0].number; | |
| } | |
| async function findSuccessfulCiRun(pullNumber, headSha) { | |
| const runs = await github.paginate( | |
| github.rest.actions.listWorkflowRunsForRepo, | |
| { | |
| owner, | |
| repo, | |
| event: 'pull_request', | |
| head_sha: headSha, | |
| per_page: 100, | |
| }, | |
| ); | |
| return runs.find((run) => | |
| run.name === ciWorkflowName && | |
| run.head_sha === headSha && | |
| run.conclusion === 'success' && | |
| (run.pull_requests ?? []).some((pullRequest) => pullRequest.number === pullNumber), | |
| ); | |
| } | |
| const number = await resolvePullRequestNumber(); | |
| if (!number) { | |
| core.setOutput('publish', 'false'); | |
| return; | |
| } | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner, | |
| repo, | |
| pull_number: number, | |
| }); | |
| if (pr.state !== 'open') { | |
| core.notice(`PR #${number} is ${pr.state}; skipping.`); | |
| core.setOutput('publish', 'false'); | |
| return; | |
| } | |
| if (pr.draft) { | |
| core.notice(`PR #${number} is draft; skipping.`); | |
| core.setOutput('publish', 'false'); | |
| return; | |
| } | |
| if (pr.base.ref !== defaultBranch) { | |
| core.notice(`PR #${number} targets ${pr.base.ref}, not ${defaultBranch}; skipping.`); | |
| core.setOutput('publish', 'false'); | |
| return; | |
| } | |
| const ciRun = context.eventName === 'workflow_run' | |
| ? context.payload.workflow_run | |
| : await findSuccessfulCiRun(number, pr.head.sha); | |
| if (!ciRun || ciRun.name !== ciWorkflowName || ciRun.conclusion !== 'success') { | |
| core.notice(`PR #${number} does not have a successful ${ciWorkflowName} run yet; skipping.`); | |
| core.setOutput('publish', 'false'); | |
| return; | |
| } | |
| const hasAutomergeLabel = pr.labels.some(({ name }) => name === 'Automerge'); | |
| if (!hasAutomergeLabel) { | |
| const files = await github.paginate(github.rest.pulls.listFiles, { | |
| owner, | |
| repo, | |
| pull_number: number, | |
| per_page: 100, | |
| }); | |
| const allowedFile = /^(Formula|Casks)\/[^/]+\.rb$/; | |
| const unexpectedFiles = files | |
| .map(({ filename }) => filename) | |
| .filter((filename) => !allowedFile.test(filename)); | |
| const trustedAutobump = | |
| pr.head.repo.full_name === `${owner}/${repo}` && | |
| pr.head.ref.startsWith('bump-') && | |
| unexpectedFiles.length === 0; | |
| if (!trustedAutobump) { | |
| core.notice(`PR #${number} is not labeled Automerge and is not a trusted autobump PR; skipping.`); | |
| core.setOutput('publish', 'false'); | |
| return; | |
| } | |
| core.notice(`Trusted autobump PR #${number} passed CI; continuing without requiring Automerge.`); | |
| } | |
| core.setOutput('publish', 'true'); | |
| core.setOutput('pr', `${number}`); | |
| core.setOutput('head_ref', pr.head.ref); | |
| core.setOutput('head_repo_clone_url', pr.head.repo.clone_url); | |
| publish: | |
| needs: | |
| - check | |
| if: needs.check.outputs.publish == 'true' | |
| runs-on: ubuntu-22.04 | |
| container: | |
| image: ghcr.io/homebrew/ubuntu22.04:master | |
| permissions: | |
| contents: write | |
| packages: write | |
| pull-requests: write | |
| env: | |
| PR: ${{ needs.check.outputs.pr }} | |
| steps: | |
| - name: Set up Homebrew | |
| id: set-up-homebrew | |
| uses: Homebrew/actions/setup-homebrew@main | |
| with: | |
| core: false | |
| cask: false | |
| test-bot: false | |
| - name: Cache Homebrew Bundler RubyGems | |
| id: cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ${{ steps.set-up-homebrew.outputs.gems-path }} | |
| key: ${{ runner.os }}-rubygems-${{ steps.set-up-homebrew.outputs.gems-hash }} | |
| restore-keys: ${{ runner.os }}-rubygems- | |
| - name: Install Homebrew Bundler RubyGems | |
| if: steps.cache.outputs.cache-hit != 'true' | |
| run: brew install-bundler-gems | |
| - name: Configure Git User | |
| run: | | |
| git config --global user.name "github-actions[bot]" | |
| git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| - name: Checkout PR Branch | |
| working-directory: ${{ steps.set-up-homebrew.outputs.repository-path }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: gh pr checkout "$PR" | |
| - name: Pull and Publish Bottles | |
| env: | |
| HOMEBREW_GITHUB_API_TOKEN: ${{ github.token }} | |
| HOMEBREW_GITHUB_PACKAGES_TOKEN: ${{ github.token }} | |
| HOMEBREW_GITHUB_PACKAGES_USER: ${{ github.actor }} | |
| PULL_REQUEST: ${{ env.PR }} | |
| run: | | |
| brew pr-pull \ | |
| --debug \ | |
| --clean \ | |
| --no-cherry-pick \ | |
| --artifact-pattern 'bottles_*' \ | |
| --tap="$GITHUB_REPOSITORY" \ | |
| --workflows ci.yml \ | |
| "$PR" | |
| - name: Push commits to PR branch | |
| uses: Homebrew/actions/git-try-push@main | |
| with: | |
| token: ${{ github.token }} | |
| branch: ${{ needs.check.outputs.head_ref }} | |
| remote: ${{ needs.check.outputs.head_repo_clone_url }} | |
| directory: ${{ steps.set-up-homebrew.outputs.repository-path }} | |
| - name: Wait for Remote Branch to be Updated | |
| working-directory: ${{ steps.set-up-homebrew.outputs.repository-path }} | |
| run: | | |
| local_head=$(git rev-parse HEAD) | |
| remote_ref="pull/${PR}/head" | |
| attempt=0 | |
| max_attempts=10 | |
| timeout=1 | |
| # Wait (with exponential backoff) until the PR branch is in sync | |
| while [[ "$attempt" -lt "$max_attempts" ]]; do | |
| remote_head="$(git ls-remote origin "$remote_ref" | cut -f1)" | |
| if [[ "$local_head" = "$remote_head" ]] | |
| then | |
| success=1 | |
| break | |
| fi | |
| sleep "$timeout" | |
| attempt=$(( attempt + 1 )) | |
| timeout=$(( timeout * 2 )) | |
| done | |
| if [[ "$success" -ne 1 ]]; then | |
| echo "Remote branch not updated after ${max_attempts} attempts" | |
| exit 1 | |
| fi | |
| - name: Approve Pull Request | |
| working-directory: ${{ steps.set-up-homebrew.outputs.repository-path }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: gh pr review "$PR" --approve | |
| - name: Enable Auto Merge | |
| working-directory: ${{ steps.set-up-homebrew.outputs.repository-path }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh pr merge "$PR" \ | |
| --merge \ | |
| --delete-branch |