feat: add FreeBSD release builds (#832)

* feat: add FreeBSD release builds

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* chore: allow manual release dispatch

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* fix: stabilize release workflow on fork

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* fix: clean zig download artifact

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* fix: use valid zig target for freebsd arm

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* fix: disable freebsd arm release build

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* fix: switch freebsd build to pure go

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* fix: skip release publishing on forks

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* fix: satisfy golangci-lint for release PR

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
Jordan Hubbard
2026-01-01 14:51:51 -04:00
committed by GitHub
parent f3f713d77a
commit aa2ea48bf2
12 changed files with 133 additions and 74 deletions

View File

@@ -4,6 +4,7 @@ on:
push: push:
tags: tags:
- 'v*' - 'v*'
workflow_dispatch:
concurrency: concurrency:
group: release-${{ github.ref }} group: release-${{ github.ref }}
@@ -36,7 +37,9 @@ jobs:
with: with:
distribution: goreleaser distribution: goreleaser
version: '~> v2' version: '~> v2'
args: release --clean args: >
release --clean
${{ github.repository != 'steveyegge/beads' && '--skip=publish --skip=announce' || '' }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Windows code signing (optional - signing is skipped if not set) # Windows code signing (optional - signing is skipped if not set)
@@ -46,7 +49,7 @@ jobs:
publish-pypi: publish-pypi:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: goreleaser needs: goreleaser
if: always() # Run even if goreleaser fails if: ${{ always() && github.repository == 'steveyegge/beads' }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v6
@@ -75,6 +78,7 @@ jobs:
publish-npm: publish-npm:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: goreleaser needs: goreleaser
if: ${{ github.repository == 'steveyegge/beads' }}
permissions: permissions:
contents: read contents: read
id-token: write # Required for npm provenance/trusted publishing id-token: write # Required for npm provenance/trusted publishing
@@ -101,6 +105,7 @@ jobs:
update-homebrew: update-homebrew:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: goreleaser needs: goreleaser
if: ${{ github.repository == 'steveyegge/beads' }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v6

View File

@@ -99,6 +99,23 @@ builds:
# Requires WINDOWS_SIGNING_CERT_PFX_BASE64 and WINDOWS_SIGNING_CERT_PASSWORD secrets # Requires WINDOWS_SIGNING_CERT_PFX_BASE64 and WINDOWS_SIGNING_CERT_PASSWORD secrets
- ./scripts/sign-windows.sh "{{ .Path }}" - ./scripts/sign-windows.sh "{{ .Path }}"
- id: bd-freebsd-amd64
main: ./cmd/bd
binary: bd
env:
- CGO_ENABLED=0
goos:
- freebsd
goarch:
- amd64
ldflags:
- -s -w
- -X main.Version={{.Version}}
- -X main.Build={{.ShortCommit}}
- -X main.Commit={{.Commit}}
- -X main.Branch={{.Branch}}
archives: archives:
- id: bd-archive - id: bd-archive
format: tar.gz format: tar.gz

View File

@@ -504,7 +504,7 @@ func discoverRigDaemons() []rigDaemon {
// Similar to routing.resolveRedirect but simplified for activity use. // Similar to routing.resolveRedirect but simplified for activity use.
func resolveBeadsRedirect(beadsDir string) string { func resolveBeadsRedirect(beadsDir string) string {
redirectFile := filepath.Join(beadsDir, "redirect") redirectFile := filepath.Join(beadsDir, "redirect")
data, err := os.ReadFile(redirectFile) data, err := os.ReadFile(redirectFile) // #nosec G304 - redirects are trusted within beads rig paths
if err != nil { if err != nil {
return beadsDir return beadsDir
} }
@@ -729,7 +729,9 @@ func runTownActivityFollow(sinceTime time.Time) {
func closeDaemons(daemons []rigDaemon) { func closeDaemons(daemons []rigDaemon) {
for _, d := range daemons { for _, d := range daemons {
if d.client != nil { if d.client != nil {
d.client.Close() if err := d.client.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to close daemon client: %v\n", err)
}
} }
} }
} }

View File

@@ -56,7 +56,7 @@ func DetectPendingMigrations(path string) []PendingMigration {
} }
// Check for missing sync-branch config (sync migration) // Check for missing sync-branch config (sync migration)
if needsSyncMigration(beadsDir, path) { if needsSyncMigration(path) {
pending = append(pending, PendingMigration{ pending = append(pending, PendingMigration{
Name: "sync", Name: "sync",
Description: "Configure sync branch for multi-clone setup", Description: "Configure sync branch for multi-clone setup",
@@ -206,7 +206,7 @@ func needsTombstonesMigration(beadsDir string) bool {
} }
// needsSyncMigration checks if sync-branch should be configured // needsSyncMigration checks if sync-branch should be configured
func needsSyncMigration(beadsDir, repoPath string) bool { func needsSyncMigration(repoPath string) bool {
// Check if already configured // Check if already configured
if syncbranch.GetFromYAML() != "" { if syncbranch.GetFromYAML() != "" {
return false return false

View File

@@ -782,7 +782,7 @@ func runPrepareCommitMsgHook(args []string) int {
} }
// Write back // Write back
if err := os.WriteFile(msgFile, []byte(sb.String()), 0644); err != nil { // #nosec G306 if err := os.WriteFile(msgFile, []byte(sb.String()), 0600); err != nil { // Restrict permissions per gosec G306
fmt.Fprintf(os.Stderr, "Warning: could not write commit message: %v\n", err) fmt.Fprintf(os.Stderr, "Warning: could not write commit message: %v\n", err)
} }

View File

@@ -854,7 +854,9 @@ func fetchJiraIssueTimestamp(ctx context.Context, jiraKey string) (time.Time, er
if err != nil { if err != nil {
return zero, fmt.Errorf("failed to fetch issue %s: %w", jiraKey, err) return zero, fmt.Errorf("failed to fetch issue %s: %w", jiraKey, err)
} }
defer resp.Body.Close() defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body) body, _ := io.ReadAll(resp.Body)

View File

@@ -774,7 +774,9 @@ var listCmd = &cobra.Command{
// Output with pager support // Output with pager support
if err := ui.ToPager(buf.String(), ui.PagerOptions{NoPager: noPager}); err != nil { if err := ui.ToPager(buf.String(), ui.PagerOptions{NoPager: noPager}); err != nil {
fmt.Fprint(os.Stdout, buf.String()) if _, writeErr := fmt.Fprint(os.Stdout, buf.String()); writeErr != nil {
fmt.Fprintf(os.Stderr, "Error writing output: %v\n", writeErr)
}
} }
// Show truncation hint if we hit the limit (GH#788) // Show truncation hint if we hit the limit (GH#788)
@@ -899,7 +901,9 @@ var listCmd = &cobra.Command{
// Output with pager support // Output with pager support
if err := ui.ToPager(buf.String(), ui.PagerOptions{NoPager: noPager}); err != nil { if err := ui.ToPager(buf.String(), ui.PagerOptions{NoPager: noPager}); err != nil {
fmt.Fprint(os.Stdout, buf.String()) if _, writeErr := fmt.Fprint(os.Stdout, buf.String()); writeErr != nil {
fmt.Fprintf(os.Stderr, "Error writing output: %v\n", writeErr)
}
} }
// Show truncation hint if we hit the limit (GH#788) // Show truncation hint if we hit the limit (GH#788)

View File

@@ -139,7 +139,9 @@ func runChecks(jsonOutput bool) {
} }
enc := json.NewEncoder(os.Stdout) enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ") enc.SetIndent("", " ")
enc.Encode(result) if err := enc.Encode(result); err != nil {
fmt.Fprintf(os.Stderr, "Error encoding preflight result: %v\n", err)
}
} else { } else {
// Human-readable output // Human-readable output
for _, r := range results { for _, r := range results {

View File

@@ -185,6 +185,7 @@ func MigrateDropEdgeColumns(db *sql.DB) error {
// Copy data from old table to new table (excluding deprecated columns) // Copy data from old table to new table (excluding deprecated columns)
// NOTE: We use fmt.Sprintf here (not db.Exec parameters) because we're interpolating // NOTE: We use fmt.Sprintf here (not db.Exec parameters) because we're interpolating
// column names/expressions, not values. db.Exec parameters only work for VALUES. // column names/expressions, not values. db.Exec parameters only work for VALUES.
// #nosec G201 - expressions are column names, not user input
copySQL := fmt.Sprintf(` copySQL := fmt.Sprintf(`
INSERT INTO issues_new ( INSERT INTO issues_new (
id, content_hash, title, description, design, acceptance_criteria, id, content_hash, title, description, design, acceptance_criteria,

View File

@@ -79,7 +79,7 @@ func CheckForcePush(ctx context.Context, store storage.Storage, repoRoot, syncBr
status.Remote = getRemoteForBranch(ctx, worktreePath, syncBranch) status.Remote = getRemoteForBranch(ctx, worktreePath, syncBranch)
// Fetch from remote to get latest state // Fetch from remote to get latest state
fetchCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "fetch", status.Remote, syncBranch) fetchCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "fetch", status.Remote, syncBranch) // #nosec G204 - repoRoot/syncBranch are validated git inputs
fetchOutput, err := fetchCmd.CombinedOutput() fetchOutput, err := fetchCmd.CombinedOutput()
if err != nil { if err != nil {
// Check if remote branch doesn't exist // Check if remote branch doesn't exist
@@ -92,7 +92,7 @@ func CheckForcePush(ctx context.Context, store storage.Storage, repoRoot, syncBr
// Get current remote SHA // Get current remote SHA
remoteRef := fmt.Sprintf("%s/%s", status.Remote, syncBranch) remoteRef := fmt.Sprintf("%s/%s", status.Remote, syncBranch)
revParseCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "rev-parse", remoteRef) revParseCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "rev-parse", remoteRef) // #nosec G204 - remoteRef constructed from trusted config
revParseOutput, err := revParseCmd.Output() revParseOutput, err := revParseCmd.Output()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get remote SHA: %w", err) return nil, fmt.Errorf("failed to get remote SHA: %w", err)
@@ -107,7 +107,7 @@ func CheckForcePush(ctx context.Context, store storage.Storage, repoRoot, syncBr
// Check if stored SHA is an ancestor of current remote SHA // Check if stored SHA is an ancestor of current remote SHA
// This means remote was updated normally (fast-forward) // This means remote was updated normally (fast-forward)
isAncestorCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "merge-base", "--is-ancestor", storedSHA, status.CurrentRemoteSHA) isAncestorCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "merge-base", "--is-ancestor", storedSHA, status.CurrentRemoteSHA) // #nosec G204 - args derive from git SHAs we validated earlier
if isAncestorCmd.Run() == nil { if isAncestorCmd.Run() == nil {
// Stored SHA is ancestor - normal update, no force-push // Stored SHA is ancestor - normal update, no force-push
status.Message = "Remote sync branch updated normally (fast-forward)" status.Message = "Remote sync branch updated normally (fast-forward)"
@@ -146,12 +146,12 @@ func UpdateStoredRemoteSHA(ctx context.Context, store storage.Storage, repoRoot,
// Get current remote SHA // Get current remote SHA
remoteRef := fmt.Sprintf("%s/%s", remote, syncBranch) remoteRef := fmt.Sprintf("%s/%s", remote, syncBranch)
revParseCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "rev-parse", remoteRef) revParseCmd := exec.CommandContext(ctx, "git", "-C", repoRoot, "rev-parse", remoteRef) // #nosec G204 - remoteRef is internal config
revParseOutput, err := revParseCmd.Output() revParseOutput, err := revParseCmd.Output()
if err != nil { if err != nil {
// Remote branch might not exist yet (first push) // Remote branch might not exist yet (first push)
// Try local branch instead // Try local branch instead
revParseCmd = exec.CommandContext(ctx, "git", "-C", repoRoot, "rev-parse", syncBranch) revParseCmd = exec.CommandContext(ctx, "git", "-C", repoRoot, "rev-parse", syncBranch) // #nosec G204 - branch name from config
revParseOutput, err = revParseCmd.Output() revParseOutput, err = revParseCmd.Output()
if err != nil { if err != nil {
return fmt.Errorf("failed to get sync branch SHA: %w", err) return fmt.Errorf("failed to get sync branch SHA: %w", err)

View File

@@ -100,7 +100,7 @@ func ToPager(content string, opts PagerOptions) error {
return nil return nil
} }
cmd := exec.Command(parts[0], parts[1:]...) cmd := exec.Command(parts[0], parts[1:]...) // #nosec G204 - pager command is user-configurable by design
cmd.Stdin = strings.NewReader(content) cmd.Stdin = strings.NewReader(content)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr

View File

@@ -34,6 +34,17 @@ log_error() {
echo -e "${RED}Error:${NC} $1" >&2 echo -e "${RED}Error:${NC} $1" >&2
} }
release_has_asset() {
local release_json=$1
local asset_name=$2
if echo "$release_json" | grep -Fq "\"name\": \"$asset_name\""; then
return 0
fi
return 1
}
# Re-sign binary for macOS to avoid slow Gatekeeper checks # Re-sign binary for macOS to avoid slow Gatekeeper checks
# See: https://github.com/steveyegge/beads/issues/466 # See: https://github.com/steveyegge/beads/issues/466
resign_for_macos() { resign_for_macos() {
@@ -70,6 +81,9 @@ detect_platform() {
Linux) Linux)
os="linux" os="linux"
;; ;;
FreeBSD)
os="freebsd"
;;
*) *)
log_error "Unsupported operating system: $(uname -s)" log_error "Unsupported operating system: $(uname -s)"
exit 1 exit 1
@@ -83,6 +97,9 @@ detect_platform() {
aarch64|arm64) aarch64|arm64)
arch="arm64" arch="arm64"
;; ;;
armv7*|armv6*|armhf|arm)
arch="arm"
;;
*) *)
log_error "Unsupported architecture: $(uname -m)" log_error "Unsupported architecture: $(uname -m)"
exit 1 exit 1
@@ -104,16 +121,19 @@ install_from_release() {
log_info "Fetching latest release..." log_info "Fetching latest release..."
local latest_url="https://api.github.com/repos/steveyegge/beads/releases/latest" local latest_url="https://api.github.com/repos/steveyegge/beads/releases/latest"
local version local version
local release_json
if command -v curl &> /dev/null; then if command -v curl &> /dev/null; then
version=$(curl -fsSL "$latest_url" | grep '"tag_name"' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/') release_json=$(curl -fsSL "$latest_url")
elif command -v wget &> /dev/null; then elif command -v wget &> /dev/null; then
version=$(wget -qO- "$latest_url" | grep '"tag_name"' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/') release_json=$(wget -qO- "$latest_url")
else else
log_error "Neither curl nor wget found. Please install one of them." log_error "Neither curl nor wget found. Please install one of them."
return 1 return 1
fi fi
version=$(echo "$release_json" | grep '"tag_name"' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/')
if [ -z "$version" ]; then if [ -z "$version" ]; then
log_error "Failed to fetch latest version" log_error "Failed to fetch latest version"
return 1 return 1
@@ -125,6 +145,12 @@ install_from_release() {
local archive_name="beads_${version#v}_${platform}.tar.gz" local archive_name="beads_${version#v}_${platform}.tar.gz"
local download_url="https://github.com/steveyegge/beads/releases/download/${version}/${archive_name}" local download_url="https://github.com/steveyegge/beads/releases/download/${version}/${archive_name}"
if ! release_has_asset "$release_json" "$archive_name"; then
log_warning "No prebuilt archive available for platform ${platform}. Falling back to source installation methods."
rm -rf "$tmp_dir"
return 1
fi
log_info "Downloading $archive_name..." log_info "Downloading $archive_name..."
cd "$tmp_dir" cd "$tmp_dir"