Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 27 additions & 21 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ name: release
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
release_tag:
description: Existing release to republish to Homebrew (no asset rebuild)
required: true
type: string

env:
RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }}

permissions:
contents: write
Expand All @@ -24,7 +33,13 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # goreleaser changelog needs the full tag history
persist-credentials: false
- name: Validate the release tag
run: |
[[ "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$ ]]
git merge-base --is-ancestor "$RELEASE_TAG" origin/main
- uses: actions/setup-go@v5
if: github.event_name == 'push'
with:
go-version-file: go.mod
cache: false # no go.sum (zero deps); silences the restore warning
Expand All @@ -37,26 +52,17 @@ jobs:
# half-published state into a clean no-op failure.
# Skipped for prerelease tags, which never touch the tap.
- name: Preflight — tap token can write the formula repo
if: ${{ !contains(github.ref_name, '-') }}
if: ${{ !contains(env.RELEASE_TAG, '-') }}
env:
GH_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
run: |
if [ -z "${GH_TOKEN:-}" ]; then
echo "::error::TAP_GITHUB_TOKEN is unset. Set it to a fine-grained PAT with Contents: write on TheWeiHu/homebrew-devbrain, then re-run this tag."
exit 1
fi
# Let jq assert the permission instead of parsing gh's stdout. An
# expired token still prints a JSON error body, and piping that through
# head/tr would mask gh's non-zero exit — the fallback would never fire.
# `jq -e` fails on false OR null, so every bad-token shape (401, 404,
# wrong repo) fails closed right here.
if ! gh api repos/TheWeiHu/homebrew-devbrain 2>/dev/null |
jq -e '.permissions.push == true' >/dev/null 2>&1; then
echo "::error::TAP_GITHUB_TOKEN cannot write TheWeiHu/homebrew-devbrain — expired, revoked, or scoped to the wrong repository. Fix it and re-run this tag; nothing has been published yet."
exit 1
fi
echo "tap token OK — homebrew-devbrain is writable"
GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
run: scripts/brew-formula-push.sh --check-auth
- name: Download existing release checksums
if: github.event_name == 'workflow_dispatch'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --pattern checksums.txt --dir dist
- uses: goreleaser/goreleaser-action@v6
if: github.event_name == 'push'
with:
version: '~> v2'
args: release --clean
Expand All @@ -65,20 +71,20 @@ jobs:
- name: Push the brew formula to the tap
env:
GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
run: scripts/brew-formula-push.sh "${GITHUB_REF_NAME#v}"
run: scripts/brew-formula-push.sh "${RELEASE_TAG#v}"

brew-canary:
# Real end-to-end check: a fresh mac can brew-install the version this tag
# just released. Only for full releases — prereleases never touch the tap.
needs: release
if: ${{ !contains(github.ref_name, '-') }}
if: ${{ !contains(inputs.release_tag || github.ref_name, '-') }}
runs-on: macos-latest
steps:
- name: brew install from the tap and assert the version
run: |
brew install TheWeiHu/devbrain/devbrain
got="$(devbrain version)"
want="${GITHUB_REF_NAME#v}"
want="${RELEASE_TAG#v}"
echo "brew served: $got · want: $want"
case "$got" in *"$want"*) ;; *) exit 1 ;; esac
devbrain doctor >/dev/null 2>&1 || true # runs; codex-only sandboxes may rc!=0
77 changes: 77 additions & 0 deletions internal/install/release_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package install

import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"

"github.com/TheWeiHu/devbrain/internal/clitest"
)

func TestTapWritePreflight(t *testing.T) {
for _, tc := range []struct {
name string
checkOnly bool
deny bool
}{
{"valid token", true, false},
{"read-only token", true, true},
{"current formula still checks write access", false, true},
{"current formula is idempotent", false, false},
} {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
stub := `#!/bin/sh
printf '%s\n' "$*" >> "$GIT_CALLS"
case " $* " in
*" clone "*)
for target do :; done
mkdir -p "$target/Formula"
;;
*" push --dry-run "*)
[ "$DENY_WRITE" = 0 ] || exit 128
;;
*" push "*) exit 99 ;;
esac
`
if err := os.WriteFile(filepath.Join(dir, "git"), []byte(stub), 0o755); err != nil {
t.Fatal(err)
}
args := []string{filepath.Join(clitest.Root(t), "scripts", "brew-formula-push.sh"), "--check-auth"}
if !tc.checkOnly {
sums := filepath.Join(dir, "checksums.txt")
var content strings.Builder
for _, platform := range []string{"darwin_amd64", "darwin_arm64", "linux_amd64", "linux_arm64"} {
content.WriteString(strings.Repeat("a", 64) + " devbrain_1.5.29_" + platform + ".tar.gz\n")
}
if err := os.WriteFile(sums, []byte(content.String()), 0o644); err != nil {
t.Fatal(err)
}
args = []string{args[0], "1.5.29", sums}
}
deny := "0"
if tc.deny {
deny = "1"
}
calls := filepath.Join(dir, "calls")
cmd := exec.Command("sh", args...)
cmd.Env = append(os.Environ(), "PATH="+dir+":"+os.Getenv("PATH"), "GITHUB_TOKEN=fixture-secret", "GIT_CALLS="+calls, "DENY_WRITE="+deny, "BREW_PUSH_DRY=")
out, err := cmd.CombinedOutput()
if (err != nil) != tc.deny {
t.Fatalf("exit = %v, denied = %v: %s", err, tc.deny, out)
}
log := mustRead(t, calls)
if !strings.Contains(log, "push --dry-run origin HEAD") {
t.Fatalf("write authentication was not exercised: %s", log)
}
if strings.Contains(log, "fixture-secret") || strings.Contains(string(out), "fixture-secret") {
t.Fatal("credential appeared in git arguments or output")
}
if !tc.checkOnly && !tc.deny && !strings.Contains(string(out), "formula already current") {
t.Fatalf("unchanged formula was not idempotent: %s", out)
}
})
}
}
31 changes: 26 additions & 5 deletions scripts/brew-formula-push.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,30 @@
# BREW_PUSH_DRY=1 renders the formula to stdout instead of pushing.
set -eu

prepare_tap() {
: "${GITHUB_TOKEN:?brew-formula-push: GITHUB_TOKEN required to push the tap}"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
cat >"$tmp/askpass" <<'ASKPASS'
#!/bin/sh
case "$1" in
*Username*) printf '%s\n' x-access-token ;;
*Password*) printf '%s\n' "$GITHUB_TOKEN" ;;
esac
ASKPASS
chmod 700 "$tmp/askpass"
export GIT_ASKPASS="$tmp/askpass" GIT_TERMINAL_PROMPT=0
git -c credential.helper= clone -q --depth 1 https://github.com/TheWeiHu/homebrew-devbrain.git "$tmp/tap"
# A repository owner's API permissions do not prove this token can write.
git -C "$tmp/tap" -c credential.helper= push --dry-run origin HEAD
}

if [ "${1:-}" = "--check-auth" ]; then
prepare_tap
echo "brew-formula-push: tap write authentication verified"
exit 0
fi

version="${1:?usage: brew-formula-push.sh <version> [checksums.txt]}"
case "$version" in
*-*) echo "brew-formula-push: prerelease/snapshot $version — skipping tap push"; exit 0 ;;
Expand Down Expand Up @@ -88,10 +112,7 @@ if [ "${BREW_PUSH_DRY:-}" = "1" ]; then
exit 0
fi

: "${GITHUB_TOKEN:?brew-formula-push: GITHUB_TOKEN required to push the tap}"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
git clone -q --depth 1 "https://x-access-token:${GITHUB_TOKEN}@github.com/TheWeiHu/homebrew-devbrain.git" "$tmp/tap"
prepare_tap
formula >"$tmp/tap/Formula/devbrain.rb"
if git -C "$tmp/tap" diff --quiet; then
echo "brew-formula-push: formula already current for v${version}"
Expand All @@ -100,5 +121,5 @@ fi
git -C "$tmp/tap" add Formula/devbrain.rb
git -C "$tmp/tap" -c user.name=devbrain-release -c user.email=release@devbrain.invalid \
commit -qm "Brew formula update for devbrain version v${version}"
git -C "$tmp/tap" push -q origin HEAD
git -C "$tmp/tap" -c credential.helper= push -q origin HEAD
echo "brew-formula-push: pushed formula for v${version}"
Loading