From 8404787c3a0385921992726c6c31d8dc8c9d6b7a Mon Sep 17 00:00:00 2001 From: Jeongkyu Shin Date: Sat, 15 Aug 2026 00:36:13 +0900 Subject: [PATCH] fix: run the Homebrew formula job automatically on release The `update-homebrew` job never ran as part of a release. `update_homebrew_formula.yml` gated its job on `github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call'`, which is unsatisfiable on the `uses:` path: inside a called workflow `github.event_name` reports the caller's originating event, which is `release` during a release run and never `workflow_call`. The job was skipped in 0s on v2.4.1 (job 91700817094) and v2.4.2 (job 94746343757), and the tap had to be bumped by a manual dispatch afterwards. That workflow declares only `workflow_dispatch` and `workflow_call` triggers, so an event gate there can only exclude legitimate runs, and it is removed. Whether the formula should be updated is now the caller's decision. A release published directly as official was a second, independent gap. `release.yml` listened only for `prereleased`, so that path started no run at all: no binaries, no assets, no formula bump. `released` is now listed alongside it, and `update-homebrew` gates on job results rather than on `publish-release` having run, so both release styles end with the tap bumped. This cannot recurse, because `publish-release` performs its flip with the default GITHUB_TOKEN and events raised by GITHUB_TOKEN do not start new workflow runs. `build` joins `update-homebrew`'s `needs` so the ordering guarantee no longer depends on `publish-release` running: the called workflow downloads the published assets and hashes them, so it must start only after all five build legs have uploaded. The condition asserts `needs.build.result == 'success'` explicitly instead of using a bare `always()` to lift the implicit `success()`, since `always()` would also fire after a failed build and push a formula whose sha256 values point at assets that were never uploaded. On a manual `workflow_dispatch` of `release.yml` the formula is updated only when the previously unused `update_homebrew` input asks for it, so rebuilding a tag by hand does not push a tap commit as a side effect. Both workflows parse under `yaml.safe_load`, the folded `if` collapses to a single-line expression, and the job graph was traced across pre-release publish, direct-official publish, build failure, publish-release failure, cancellation, and both manual dispatch variants. Manual `workflow_dispatch` of `update_homebrew_formula.yml` with an explicit `release_tag` stays the recovery path and is unchanged. Refs #266 --- .github/workflows/release.yml | 47 +++++++++++++++++-- .github/workflows/update_homebrew_formula.yml | 15 +++++- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9c1b28dd..688957c0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,23 @@ name: Release on: + # Both ways of cutting a release are supported, and both end with the Homebrew + # tap bumped automatically: + # + # prereleased - the release is published as a pre-release. `build` uploads + # every asset, `publish-release` then flips the release to official, and + # `update-homebrew` bumps the tap. + # released - the release is published directly as official (or an existing + # pre-release is flipped by hand). `publish-release` is skipped because + # there is nothing to convert, so `update-homebrew` keys off `build`. + # + # Before `released` was listed here, a release published directly as official + # started no run at all: no binaries, no assets, no formula bump. Listing it + # cannot loop back on us either, because `publish-release` performs its flip + # with the default GITHUB_TOKEN and events raised by GITHUB_TOKEN do not start + # new workflow runs. release: - types: [prereleased] + types: [prereleased, released] workflow_dispatch: inputs: update_homebrew: @@ -263,14 +278,38 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # ============================================================================ - # Update Homebrew formula (after publish converts pre-release to official) + # Update Homebrew formula (after every release asset has been uploaded) # ============================================================================ + # + # `build` is listed in `needs` explicitly rather than relied on transitively + # through `publish-release`. The called workflow downloads the published + # assets and hashes them, so it must never start before all five build legs + # have finished uploading, and that guarantee has to hold on the path where + # `publish-release` does not run at all. `publish-release` stays in `needs` so + # the pre-release path is still ordered behind the flip to official. + # + # `!cancelled()` is required: `publish-release` is legitimately skipped when + # the release was published as official already, and under the implicit + # `success()` a job whose dependency was skipped is skipped too. A bare + # `always()` would be the wrong way to lift that, since it would also fire + # after a failed build and push a formula whose sha256 values point at assets + # that were never uploaded, so the build result is asserted explicitly instead + # of leaning on job ordering. + # + # On a manual `workflow_dispatch` of this workflow the formula is updated only + # when the `update_homebrew` input asks for it, so rebuilding a tag by hand + # does not push a tap commit as a side effect. update-homebrew: name: Update Homebrew formula - needs: [publish-release] + needs: [build, publish-release] + if: >- + !cancelled() + && needs.build.result == 'success' + && (needs.publish-release.result == 'success' || needs.publish-release.result == 'skipped') + && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.update_homebrew == 'true')) uses: ./.github/workflows/update_homebrew_formula.yml with: - release_tag: ${{ github.event.release.tag_name }} + release_tag: ${{ github.event.release.tag_name || github.event.inputs.release_tag }} secrets: inherit # ============================================================================ diff --git a/.github/workflows/update_homebrew_formula.yml b/.github/workflows/update_homebrew_formula.yml index c56073e8..6fc1c2e2 100644 --- a/.github/workflows/update_homebrew_formula.yml +++ b/.github/workflows/update_homebrew_formula.yml @@ -23,7 +23,20 @@ jobs: runs-on: macos-latest environment: packaging - if: github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call' + # No job-level `if` here, on purpose. This job used to carry + # + # if: github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call' + # + # which is unsatisfiable on the `uses:` path. Inside a called workflow + # `github.event_name` reports the *caller's* originating event, which is + # `release` during a release run, and never `workflow_call`. So the job was + # skipped in 0s on every release (v2.4.1 job 91700817094, v2.4.2 job + # 94746343757) and the tap had to be bumped afterwards by hand. + # + # The `on:` block above already declares the only two ways this workflow can + # start, so an event gate here can only exclude legitimate runs. Whether the + # formula should be updated at all is the caller's decision; see the + # `update-homebrew` job in release.yml. steps: - name: Checkout this repository