Skip to content

Post-Release Formula Update #26

Post-Release Formula Update

Post-Release Formula Update #26

name: Post-Release Formula Update
# Runs after the Release workflow finishes uploading binaries + their .sha256
# sidecars. Pulls the real shas, rewrites the formula in CorvidLabs/homebrew-tap
# with the new version + shas. This is the only correct moment to bump the
# formula β€” at `fledge release` time the new version's binaries don't exist
# yet, so any pre-build sha would be a lie.
#
# Security note: every value derived from `github.event.workflow_run.*` (or
# any other potentially attacker-controlled context) flows through an `env:`
# block before reaching a shell. Direct `${{ … }}` interpolation inside
# `run: |` is a code-injection sink and is intentionally avoided here.
permissions:
contents: read
on:
workflow_run:
workflows: ["Release"]
types: [completed]
# Manual recovery lever: re-sync the formula for a given tag without
# re-triggering the whole Release workflow (e.g. after rotating an expired
# TAP_GITHUB_TOKEN). The workflow_run-only trigger previously left no way to
# recover a failed formula update except re-running a past workflow_run.
workflow_dispatch:
inputs:
tag:
description: "Release tag to sync (e.g. v1.6.0)"
required: true
type: string
jobs:
update-formula:
if: ${{ github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && startsWith(github.event.workflow_run.head_branch, 'v')) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: main
- name: Resolve release tag
id: tag
env:
# workflow_run supplies the tag via head_branch; manual dispatch via input.
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch || github.event.inputs.tag }}
run: |
set -euo pipefail
if ! [[ "$HEAD_BRANCH" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$ ]]; then
echo "::error::Refusing to run with non-semver tag '$HEAD_BRANCH'"
exit 1
fi
{
echo "tag=$HEAD_BRANCH"
echo "version=${HEAD_BRANCH#v}"
} >> "$GITHUB_OUTPUT"
- name: Fetch sha256 sidecars from release
id: shas
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.tag.outputs.tag }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
fetch() {
gh release download "$TAG" -p "$1.sha256" -R "$REPO" -O - \
| awk '{print $1}'
}
validate() {
local sha="$1"
if ! [[ "$sha" =~ ^[0-9a-fA-F]{64}$ ]]; then
echo "::error::Invalid sha256 from sidecar: '$sha'"
exit 1
fi
printf '%s' "$sha"
}
{
echo "macos_aarch64=$(validate "$(fetch fledge-macos-aarch64)")"
echo "macos_x86_64=$(validate "$(fetch fledge-macos-x86_64)")"
echo "linux_x86_64=$(validate "$(fetch fledge-linux-x86_64)")"
} >> "$GITHUB_OUTPUT"
- name: Sync formula to homebrew-tap
env:
GH_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
NEW_VERSION: ${{ steps.tag.outputs.version }}
MACOS_AARCH64: ${{ steps.shas.outputs.macos_aarch64 }}
MACOS_X86_64: ${{ steps.shas.outputs.macos_x86_64 }}
LINUX_X86_64: ${{ steps.shas.outputs.linux_x86_64 }}
run: |
set -euo pipefail
if [ -z "$GH_TOKEN" ]; then
echo "::error::TAP_GITHUB_TOKEN secret is required"
exit 1
fi
git clone "https://x-access-token:${GH_TOKEN}@github.com/CorvidLabs/homebrew-tap.git" /tmp/homebrew-tap
cd /tmp/homebrew-tap
# Rewrite the formula in-place
python3 - <<'PY'
import os, re, sys, pathlib
p = pathlib.Path("Formula/fledge.rb")
src = p.read_text()
src = re.sub(
r'(?m)^(\s*version\s+")(\d+\.\d+\.\d+)(")',
rf'\g<1>{os.environ["NEW_VERSION"]}\g<3>',
src,
count=1,
)
shas = [
os.environ["MACOS_AARCH64"],
os.environ["MACOS_X86_64"],
os.environ["LINUX_X86_64"],
]
parts = re.split(r'(sha256\s+"[0-9a-fA-F]{64}")', src)
if len(parts) - 1 != 6:
sys.exit(
f"Expected 3 sha256 lines in Formula/fledge.rb, found {(len(parts) - 1) // 2}"
)
for i, sha in enumerate(shas):
parts[2 * i + 1] = f'sha256 "{sha}"'
p.write_text("".join(parts))
PY
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if git diff --quiet -- Formula/fledge.rb; then
echo "homebrew-tap Formula/fledge.rb already at v$NEW_VERSION β€” nothing to do."
exit 0
fi
git add Formula/fledge.rb
git commit -m "chore: update formula to v$NEW_VERSION"
git push origin main
- name: Verify channels converged
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NEW_VERSION: ${{ steps.tag.outputs.version }}
run: |
set -euo pipefail
# Assert the pushed formula version actually took effect. GitHub's
# Contents API can serve a cached blob for several seconds after a
# push, so poll with a short backoff before declaring failure β€”
# otherwise a sync that DID land false-fails as "did not take effect"
# (observed on the v1.6.0 backfill). This still turns a genuine no-op
# push into a red run, just without the propagation race.
tap_version=""
for attempt in 1 2 3 4 5 6; do
formula="$(gh api repos/CorvidLabs/homebrew-tap/contents/Formula/fledge.rb --jq '.content' | base64 --decode)"
tap_version="$(printf '%s' "$formula" | sed -n 's/^[[:space:]]*version[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)"
[ "$tap_version" = "$NEW_VERSION" ] && break
echo "Formula still reports '$tap_version' (attempt $attempt/6); waiting for Contents API to propagate..."
sleep 10
done
if [ "$tap_version" != "$NEW_VERSION" ]; then
echo "::error::homebrew-tap Formula/fledge.rb reports '$tap_version', expected '$NEW_VERSION' after retries β€” formula sync did not take effect."
exit 1
fi
echo "Verified homebrew-tap Formula/fledge.rb at v$NEW_VERSION."
# crates.io is published out of band; warn (don't fail) if it lags so
# channel drift is at least visible in the run log.
crate_version="$(curl -fsSL -A 'fledge-ci' https://crates.io/api/v1/crates/fledge \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["crate"]["max_version"])' 2>/dev/null || echo "")"
if [ -n "$crate_version" ] && [ "$crate_version" != "$NEW_VERSION" ]; then
echo "::warning::crates.io max_version '$crate_version' != release '$NEW_VERSION' β€” crates.io publish may be pending or missing."
fi