From 38cc12eb8f134a8fc6437489fc5adffcd3a8337e Mon Sep 17 00:00:00 2001 From: Jahvon Dockery Date: Thu, 27 Aug 2026 11:31:40 -0400 Subject: [PATCH 1/2] fix(ci): decouple homebrew tap push from release and docs deploy The v2.2.0 release failed on a single 403 pushing the cask to flowexec/homebrew-tap. Because goreleaser does the cask push as part of `goreleaser release`, that one credential problem aborted the whole release job, which cost two things nobody would expect: - the ghcr multi-arch publish never ran (failFast on the serial) - release-docs was skipped via `needs: release-binary`, so the JSON schemas the site serves stayed frozen at 2026-05-02 - exactly the staleness #452 existed to prevent Three changes: - goreleaser generates the cask into dist/ but no longer uploads it (skip_upload). The push is a continue-on-error workflow step, so a tap credential failure costs the tap only. - release-docs now gates on a `published` job output, set by checking that the GitHub release exists, rather than on release-binary's conclusion. Docs ship whenever the release actually cut. - Create Tag is idempotent, so a release that failed partway can be re-dispatched with the same tag instead of dying on an existing one. A failed cask push writes a warning to the job summary so the non-fatal step cannot fail silently. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012mnVfKRbSbYV43gdjcdxdX --- .execs/release.flow | 2 +- .github/workflows/release.yaml | 101 +++++++++++++++++++++++++++++++-- .goreleaser.yaml | 5 +- 3 files changed, 101 insertions(+), 7 deletions(-) diff --git a/.execs/release.flow b/.execs/release.flow index 1fe0dc60..81d720a0 100644 --- a/.execs/release.flow +++ b/.execs/release.flow @@ -192,8 +192,8 @@ executables: echo "" echo "✓ Release ${VERSION} complete!" echo " - Binaries published to GitHub Releases" - echo " - Homebrew tap updated" echo " - Docker images published to ghcr.io" + echo " - Homebrew cask generated (published by the release workflow)" - verb: check name: release diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1778b48e..6a2a2319 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -16,6 +16,12 @@ jobs: contents: write packages: write runs-on: ubuntu-latest + outputs: + # Gates the docs deploy on the release actually existing rather than on this + # job's overall conclusion, so a failure in a downstream distribution channel + # (Homebrew, ghcr) cannot skip the schema publish. + published: ${{ steps.released.outputs.published }} + version: ${{ steps.version.outputs.version }} steps: - uses: actions/checkout@v7 with: @@ -34,13 +40,26 @@ jobs: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + + # Idempotent so a release that failed partway (e.g. a bad tap credential) + # can be re-dispatched with the same tag instead of dying here. - name: Create Tag if: ${{ github.event.inputs.tag != '' }} - run: | - git tag ${{ github.event.inputs.tag }} - git push origin ${{ github.event.inputs.tag }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.event.inputs.tag }} + run: | + set -euo pipefail + if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then + echo "Tag ${TAG} already exists locally - reusing" + elif git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then + echo "Tag ${TAG} already exists on origin - fetching" + git fetch origin "refs/tags/${TAG}:refs/tags/${TAG}" + else + git tag "${TAG}" + git push origin "${TAG}" + fi + - name: Extract version from tag id: version run: | @@ -51,8 +70,12 @@ jobs: fi echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "Releasing version: ${VERSION}" + - name: Install release tools run: go install github.com/goreleaser/goreleaser/v2@v2.12.5 + + # Binaries, GitHub release, and ghcr images. The Homebrew cask is generated + # into dist/ here but NOT pushed - see the publish-tap step below. - uses: flowexec/action@v1 with: executable: 'publish release --param VERSION=${{ steps.version.outputs.version }}' @@ -60,7 +83,73 @@ jobs: flow-version: 'main' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Records whether the GitHub release exists, regardless of what happened + # after it was cut. Runs even on failure so the docs gate is accurate. + - name: Check release published + id: released + if: always() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if gh release view "${{ steps.version.outputs.version }}" \ + --repo "${{ github.repository }}" >/dev/null 2>&1; then + echo "published=true" >> $GITHUB_OUTPUT + echo "Release ${{ steps.version.outputs.version }} is published." + else + echo "published=false" >> $GITHUB_OUTPUT + echo "Release ${{ steps.version.outputs.version }} was NOT published." + fi + + # The tap is a distribution side-channel with its own credential. A 403 here + # must not cost us the ghcr images or the schema deploy, so it is explicitly + # non-fatal and reported in the job summary instead. + - name: Publish Homebrew cask + id: tap + if: ${{ always() && steps.released.outputs.published == 'true' }} + continue-on-error: true + env: HOMEBREW_FLOW_GITHUB_TOKEN: ${{ secrets.HOMEBREW_FLOW_GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} + run: | + set -euo pipefail + if [ -z "${HOMEBREW_FLOW_GITHUB_TOKEN:-}" ]; then + echo "HOMEBREW_FLOW_GITHUB_TOKEN is not set" >&2 + exit 1 + fi + CASK="dist/homebrew/Casks/flow.rb" + test -f "$CASK" || { echo "cask not generated at $CASK" >&2; exit 1; } + + TMP="$(mktemp -d)" + git clone --depth 1 \ + "https://x-access-token:${HOMEBREW_FLOW_GITHUB_TOKEN}@github.com/flowexec/homebrew-tap.git" \ + "$TMP" + mkdir -p "$TMP/Casks" + cp "$CASK" "$TMP/Casks/flow.rb" + git -C "$TMP" add Casks/flow.rb + if git -C "$TMP" diff --cached --quiet; then + echo "Cask already up to date for ${VERSION}" + exit 0 + fi + git -C "$TMP" -c user.name=goreleaserbot \ + -c user.email=bot@goreleaser.com \ + commit -m "Brew cask update for flow version ${VERSION}" + git -C "$TMP" push origin HEAD + + - name: Report tap outcome + if: ${{ always() && steps.released.outputs.published == 'true' }} + run: | + if [ "${{ steps.tap.outcome }}" = "success" ]; then + echo "- Homebrew tap updated to ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + else + { + echo "### :warning: Homebrew tap NOT updated" + echo "" + echo "The release published, but pushing the cask to \`flowexec/homebrew-tap\` failed." + echo "\`brew upgrade\` will keep serving the previous version until this is resolved." + echo "Check \`HOMEBREW_FLOW_GITHUB_TOKEN\` (needs Contents: Read and write on the tap repo)." + } >> $GITHUB_STEP_SUMMARY + fi # Publishing the docs is part of releasing: the site serves the JSON schemas # that editors validate flowfiles against, so skipping it ships a release whose @@ -68,9 +157,11 @@ jobs: # because a tag pushed with GITHUB_TOKEN does not trigger workflows. release-docs: needs: release-binary - # Only for a dispatched release. A tag pushed by a human triggers + # Gate on the release having been published, not on release-binary's overall + # conclusion - a Homebrew or ghcr failure must not stop the schemas shipping. + # Only for a dispatched release: a tag pushed by a human triggers # release-docs.yaml on its own, and calling it here too would deploy twice. - if: github.event_name == 'workflow_dispatch' + if: ${{ always() && github.event_name == 'workflow_dispatch' && needs.release-binary.outputs.published == 'true' }} permissions: contents: read uses: ./.github/workflows/release-docs.yaml diff --git a/.goreleaser.yaml b/.goreleaser.yaml index a271c713..b6a41053 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -49,10 +49,13 @@ homebrew_casks: binary: flow homepage: https://flowexec.io license: Apache-2.0 + # The cask is generated into dist/ and pushed by the release workflow's + # "Publish Homebrew cask" step. Keeping the upload out of goreleaser means a + # tap credential failure cannot abort the GitHub release or the ghcr publish. + skip_upload: "true" repository: owner: flowexec name: homebrew-tap - token: "{{ .Env.HOMEBREW_FLOW_GITHUB_TOKEN }}" completions: bash: scripts/completions/flow.bash zsh: scripts/completions/flow.zsh From 2bdf8f3d6816c1146c208ccb429c3c3c80ba94fb Mon Sep 17 00:00:00 2001 From: Jahvon Dockery Date: Thu, 27 Aug 2026 11:59:58 -0400 Subject: [PATCH 2/2] revert(ci): keep the homebrew cask push inside goreleaser The tap credential is fixed, so goreleaser can push the cask again. Restores .goreleaser.yaml and .execs/release.flow to their previous state and drops the separate cask-publish step. The docs gate and the idempotent tag creation stay: neither depends on the tap, and the docs gate is what stops a late-stage release failure from skipping the schema deploy. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012mnVfKRbSbYV43gdjcdxdX --- .execs/release.flow | 2 +- .github/workflows/release.yaml | 53 +--------------------------------- .goreleaser.yaml | 5 +--- 3 files changed, 3 insertions(+), 57 deletions(-) diff --git a/.execs/release.flow b/.execs/release.flow index 81d720a0..1fe0dc60 100644 --- a/.execs/release.flow +++ b/.execs/release.flow @@ -192,8 +192,8 @@ executables: echo "" echo "✓ Release ${VERSION} complete!" echo " - Binaries published to GitHub Releases" + echo " - Homebrew tap updated" echo " - Docker images published to ghcr.io" - echo " - Homebrew cask generated (published by the release workflow)" - verb: check name: release diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6a2a2319..0b417567 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -74,8 +74,6 @@ jobs: - name: Install release tools run: go install github.com/goreleaser/goreleaser/v2@v2.12.5 - # Binaries, GitHub release, and ghcr images. The Homebrew cask is generated - # into dist/ here but NOT pushed - see the publish-tap step below. - uses: flowexec/action@v1 with: executable: 'publish release --param VERSION=${{ steps.version.outputs.version }}' @@ -83,6 +81,7 @@ jobs: flow-version: 'main' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HOMEBREW_FLOW_GITHUB_TOKEN: ${{ secrets.HOMEBREW_FLOW_GITHUB_TOKEN }} # Records whether the GitHub release exists, regardless of what happened # after it was cut. Runs even on failure so the docs gate is accurate. @@ -101,56 +100,6 @@ jobs: echo "Release ${{ steps.version.outputs.version }} was NOT published." fi - # The tap is a distribution side-channel with its own credential. A 403 here - # must not cost us the ghcr images or the schema deploy, so it is explicitly - # non-fatal and reported in the job summary instead. - - name: Publish Homebrew cask - id: tap - if: ${{ always() && steps.released.outputs.published == 'true' }} - continue-on-error: true - env: - HOMEBREW_FLOW_GITHUB_TOKEN: ${{ secrets.HOMEBREW_FLOW_GITHUB_TOKEN }} - VERSION: ${{ steps.version.outputs.version }} - run: | - set -euo pipefail - if [ -z "${HOMEBREW_FLOW_GITHUB_TOKEN:-}" ]; then - echo "HOMEBREW_FLOW_GITHUB_TOKEN is not set" >&2 - exit 1 - fi - CASK="dist/homebrew/Casks/flow.rb" - test -f "$CASK" || { echo "cask not generated at $CASK" >&2; exit 1; } - - TMP="$(mktemp -d)" - git clone --depth 1 \ - "https://x-access-token:${HOMEBREW_FLOW_GITHUB_TOKEN}@github.com/flowexec/homebrew-tap.git" \ - "$TMP" - mkdir -p "$TMP/Casks" - cp "$CASK" "$TMP/Casks/flow.rb" - git -C "$TMP" add Casks/flow.rb - if git -C "$TMP" diff --cached --quiet; then - echo "Cask already up to date for ${VERSION}" - exit 0 - fi - git -C "$TMP" -c user.name=goreleaserbot \ - -c user.email=bot@goreleaser.com \ - commit -m "Brew cask update for flow version ${VERSION}" - git -C "$TMP" push origin HEAD - - - name: Report tap outcome - if: ${{ always() && steps.released.outputs.published == 'true' }} - run: | - if [ "${{ steps.tap.outcome }}" = "success" ]; then - echo "- Homebrew tap updated to ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY - else - { - echo "### :warning: Homebrew tap NOT updated" - echo "" - echo "The release published, but pushing the cask to \`flowexec/homebrew-tap\` failed." - echo "\`brew upgrade\` will keep serving the previous version until this is resolved." - echo "Check \`HOMEBREW_FLOW_GITHUB_TOKEN\` (needs Contents: Read and write on the tap repo)." - } >> $GITHUB_STEP_SUMMARY - fi - # Publishing the docs is part of releasing: the site serves the JSON schemas # that editors validate flowfiles against, so skipping it ships a release whose # schema the world cannot see. This runs here rather than off the tag push diff --git a/.goreleaser.yaml b/.goreleaser.yaml index b6a41053..a271c713 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -49,13 +49,10 @@ homebrew_casks: binary: flow homepage: https://flowexec.io license: Apache-2.0 - # The cask is generated into dist/ and pushed by the release workflow's - # "Publish Homebrew cask" step. Keeping the upload out of goreleaser means a - # tap credential failure cannot abort the GitHub release or the ghcr publish. - skip_upload: "true" repository: owner: flowexec name: homebrew-tap + token: "{{ .Env.HOMEBREW_FLOW_GITHUB_TOKEN }}" completions: bash: scripts/completions/flow.bash zsh: scripts/completions/flow.zsh