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:
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user