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:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
@@ -36,7 +37,9 @@ jobs:
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: '~> v2'
|
||||
args: release --clean
|
||||
args: >
|
||||
release --clean
|
||||
${{ github.repository != 'steveyegge/beads' && '--skip=publish --skip=announce' || '' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Windows code signing (optional - signing is skipped if not set)
|
||||
@@ -46,7 +49,7 @@ jobs:
|
||||
publish-pypi:
|
||||
runs-on: ubuntu-latest
|
||||
needs: goreleaser
|
||||
if: always() # Run even if goreleaser fails
|
||||
if: ${{ always() && github.repository == 'steveyegge/beads' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -75,6 +78,7 @@ jobs:
|
||||
publish-npm:
|
||||
runs-on: ubuntu-latest
|
||||
needs: goreleaser
|
||||
if: ${{ github.repository == 'steveyegge/beads' }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # Required for npm provenance/trusted publishing
|
||||
@@ -101,6 +105,7 @@ jobs:
|
||||
update-homebrew:
|
||||
runs-on: ubuntu-latest
|
||||
needs: goreleaser
|
||||
if: ${{ github.repository == 'steveyegge/beads' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
@@ -99,6 +99,23 @@ builds:
|
||||
# Requires WINDOWS_SIGNING_CERT_PFX_BASE64 and WINDOWS_SIGNING_CERT_PASSWORD secrets
|
||||
- ./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:
|
||||
- id: bd-archive
|
||||
format: tar.gz
|
||||
|
||||
@@ -504,7 +504,7 @@ func discoverRigDaemons() []rigDaemon {
|
||||
// Similar to routing.resolveRedirect but simplified for activity use.
|
||||
func resolveBeadsRedirect(beadsDir string) string {
|
||||
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 {
|
||||
return beadsDir
|
||||
}
|
||||
@@ -729,7 +729,9 @@ func runTownActivityFollow(sinceTime time.Time) {
|
||||
func closeDaemons(daemons []rigDaemon) {
|
||||
for _, d := range daemons {
|
||||
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)
|
||||
if needsSyncMigration(beadsDir, path) {
|
||||
if needsSyncMigration(path) {
|
||||
pending = append(pending, PendingMigration{
|
||||
Name: "sync",
|
||||
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
|
||||
func needsSyncMigration(beadsDir, repoPath string) bool {
|
||||
func needsSyncMigration(repoPath string) bool {
|
||||
// Check if already configured
|
||||
if syncbranch.GetFromYAML() != "" {
|
||||
return false
|
||||
|
||||
@@ -782,7 +782,7 @@ func runPrepareCommitMsgHook(args []string) int {
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,22 +23,22 @@ import (
|
||||
|
||||
// JiraSyncStats tracks statistics for a Jira sync operation.
|
||||
type JiraSyncStats struct {
|
||||
Pulled int `json:"pulled"`
|
||||
Pushed int `json:"pushed"`
|
||||
Created int `json:"created"`
|
||||
Updated int `json:"updated"`
|
||||
Skipped int `json:"skipped"`
|
||||
Errors int `json:"errors"`
|
||||
Pulled int `json:"pulled"`
|
||||
Pushed int `json:"pushed"`
|
||||
Created int `json:"created"`
|
||||
Updated int `json:"updated"`
|
||||
Skipped int `json:"skipped"`
|
||||
Errors int `json:"errors"`
|
||||
Conflicts int `json:"conflicts"`
|
||||
}
|
||||
|
||||
// JiraSyncResult represents the result of a Jira sync operation.
|
||||
type JiraSyncResult struct {
|
||||
Success bool `json:"success"`
|
||||
Stats JiraSyncStats `json:"stats"`
|
||||
LastSync string `json:"last_sync,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
Success bool `json:"success"`
|
||||
Stats JiraSyncStats `json:"stats"`
|
||||
LastSync string `json:"last_sync,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
}
|
||||
|
||||
var jiraCmd = &cobra.Command{
|
||||
@@ -288,13 +288,13 @@ var jiraStatusCmd = &cobra.Command{
|
||||
|
||||
if jsonOutput {
|
||||
outputJSON(map[string]interface{}{
|
||||
"configured": configured,
|
||||
"jira_url": jiraURL,
|
||||
"jira_project": jiraProject,
|
||||
"last_sync": lastSync,
|
||||
"total_issues": len(allIssues),
|
||||
"with_jira_ref": withJiraRef,
|
||||
"pending_push": pendingPush,
|
||||
"configured": configured,
|
||||
"jira_url": jiraURL,
|
||||
"jira_project": jiraProject,
|
||||
"last_sync": lastSync,
|
||||
"total_issues": len(allIssues),
|
||||
"with_jira_ref": withJiraRef,
|
||||
"pending_push": pendingPush,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -610,9 +610,9 @@ Looked in: %v`, name, locations)
|
||||
|
||||
// JiraConflict represents a conflict between local and Jira versions.
|
||||
type JiraConflict struct {
|
||||
IssueID string
|
||||
LocalUpdated time.Time
|
||||
JiraUpdated time.Time
|
||||
IssueID string
|
||||
LocalUpdated time.Time
|
||||
JiraUpdated time.Time
|
||||
JiraExternalRef string
|
||||
}
|
||||
|
||||
@@ -854,7 +854,9 @@ func fetchJiraIssueTimestamp(ctx context.Context, jiraKey string) (time.Time, er
|
||||
if err != nil {
|
||||
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 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
@@ -385,12 +385,12 @@ var listCmd = &cobra.Command{
|
||||
longFormat, _ := cmd.Flags().GetBool("long")
|
||||
sortBy, _ := cmd.Flags().GetString("sort")
|
||||
reverse, _ := cmd.Flags().GetBool("reverse")
|
||||
|
||||
|
||||
// Pattern matching flags
|
||||
titleContains, _ := cmd.Flags().GetString("title-contains")
|
||||
descContains, _ := cmd.Flags().GetString("desc-contains")
|
||||
notesContains, _ := cmd.Flags().GetString("notes-contains")
|
||||
|
||||
|
||||
// Date range flags
|
||||
createdAfter, _ := cmd.Flags().GetString("created-after")
|
||||
createdBefore, _ := cmd.Flags().GetString("created-before")
|
||||
@@ -398,12 +398,12 @@ var listCmd = &cobra.Command{
|
||||
updatedBefore, _ := cmd.Flags().GetString("updated-before")
|
||||
closedAfter, _ := cmd.Flags().GetString("closed-after")
|
||||
closedBefore, _ := cmd.Flags().GetString("closed-before")
|
||||
|
||||
|
||||
// Empty/null check flags
|
||||
emptyDesc, _ := cmd.Flags().GetBool("empty-description")
|
||||
noAssignee, _ := cmd.Flags().GetBool("no-assignee")
|
||||
noLabels, _ := cmd.Flags().GetBool("no-labels")
|
||||
|
||||
|
||||
// Priority range flags
|
||||
priorityMinStr, _ := cmd.Flags().GetString("priority-min")
|
||||
priorityMaxStr, _ := cmd.Flags().GetString("priority-max")
|
||||
@@ -509,7 +509,7 @@ var listCmd = &cobra.Command{
|
||||
filter.IDs = ids
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Pattern matching
|
||||
if titleContains != "" {
|
||||
filter.TitleContains = titleContains
|
||||
@@ -520,7 +520,7 @@ var listCmd = &cobra.Command{
|
||||
if notesContains != "" {
|
||||
filter.NotesContains = notesContains
|
||||
}
|
||||
|
||||
|
||||
// Date ranges
|
||||
if createdAfter != "" {
|
||||
t, err := parseTimeFlag(createdAfter)
|
||||
@@ -570,7 +570,7 @@ var listCmd = &cobra.Command{
|
||||
}
|
||||
filter.ClosedBefore = &t
|
||||
}
|
||||
|
||||
|
||||
// Empty/null checks
|
||||
if emptyDesc {
|
||||
filter.EmptyDescription = true
|
||||
@@ -581,7 +581,7 @@ var listCmd = &cobra.Command{
|
||||
if noLabels {
|
||||
filter.NoLabels = true
|
||||
}
|
||||
|
||||
|
||||
// Priority ranges
|
||||
if cmd.Flags().Changed("priority-min") {
|
||||
priorityMin, err := validation.ValidatePriority(priorityMinStr)
|
||||
@@ -640,7 +640,7 @@ var listCmd = &cobra.Command{
|
||||
}
|
||||
}
|
||||
|
||||
// If daemon is running, use RPC
|
||||
// If daemon is running, use RPC
|
||||
if daemonClient != nil {
|
||||
listArgs := &rpc.ListArgs{
|
||||
Status: status,
|
||||
@@ -665,17 +665,17 @@ var listCmd = &cobra.Command{
|
||||
}
|
||||
// Forward title search via Query field (searches title/description/id)
|
||||
if titleSearch != "" {
|
||||
listArgs.Query = titleSearch
|
||||
listArgs.Query = titleSearch
|
||||
}
|
||||
if len(filter.IDs) > 0 {
|
||||
listArgs.IDs = filter.IDs
|
||||
}
|
||||
|
||||
if len(filter.IDs) > 0 {
|
||||
listArgs.IDs = filter.IDs
|
||||
}
|
||||
|
||||
// Pattern matching
|
||||
listArgs.TitleContains = titleContains
|
||||
listArgs.DescriptionContains = descContains
|
||||
listArgs.NotesContains = notesContains
|
||||
|
||||
|
||||
// Date ranges
|
||||
if filter.CreatedAfter != nil {
|
||||
listArgs.CreatedAfter = filter.CreatedAfter.Format(time.RFC3339)
|
||||
@@ -695,12 +695,12 @@ var listCmd = &cobra.Command{
|
||||
if filter.ClosedBefore != nil {
|
||||
listArgs.ClosedBefore = filter.ClosedBefore.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
|
||||
// Empty/null checks
|
||||
listArgs.EmptyDescription = filter.EmptyDescription
|
||||
listArgs.NoAssignee = filter.NoAssignee
|
||||
listArgs.NoLabels = filter.NoLabels
|
||||
|
||||
|
||||
// Priority range
|
||||
listArgs.PriorityMin = filter.PriorityMin
|
||||
listArgs.PriorityMax = filter.PriorityMax
|
||||
@@ -721,7 +721,7 @@ var listCmd = &cobra.Command{
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := daemonClient.List(listArgs)
|
||||
resp, err := daemonClient.List(listArgs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -774,7 +774,9 @@ var listCmd = &cobra.Command{
|
||||
|
||||
// Output with pager support
|
||||
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)
|
||||
@@ -788,21 +790,21 @@ var listCmd = &cobra.Command{
|
||||
// ctx already created above for staleness check
|
||||
issues, err := store.SearchIssues(ctx, "", filter)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// If no issues found, check if git has issues and auto-import
|
||||
if len(issues) == 0 {
|
||||
if checkAndAutoImport(ctx, store) {
|
||||
// Re-run the query after import
|
||||
issues, err = store.SearchIssues(ctx, "", filter)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
// If no issues found, check if git has issues and auto-import
|
||||
if len(issues) == 0 {
|
||||
if checkAndAutoImport(ctx, store) {
|
||||
// Re-run the query after import
|
||||
issues, err = store.SearchIssues(ctx, "", filter)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
sortIssues(issues, sortBy, reverse)
|
||||
@@ -899,7 +901,9 @@ var listCmd = &cobra.Command{
|
||||
|
||||
// Output with pager support
|
||||
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)
|
||||
@@ -927,12 +931,12 @@ func init() {
|
||||
listCmd.Flags().Bool("long", false, "Show detailed multi-line output for each issue")
|
||||
listCmd.Flags().String("sort", "", "Sort by field: priority, created, updated, closed, status, id, title, type, assignee")
|
||||
listCmd.Flags().BoolP("reverse", "r", false, "Reverse sort order")
|
||||
|
||||
|
||||
// Pattern matching
|
||||
listCmd.Flags().String("title-contains", "", "Filter by title substring (case-insensitive)")
|
||||
listCmd.Flags().String("desc-contains", "", "Filter by description substring (case-insensitive)")
|
||||
listCmd.Flags().String("notes-contains", "", "Filter by notes substring (case-insensitive)")
|
||||
|
||||
|
||||
// Date ranges
|
||||
listCmd.Flags().String("created-after", "", "Filter issues created after date (YYYY-MM-DD or RFC3339)")
|
||||
listCmd.Flags().String("created-before", "", "Filter issues created before date (YYYY-MM-DD or RFC3339)")
|
||||
@@ -940,12 +944,12 @@ func init() {
|
||||
listCmd.Flags().String("updated-before", "", "Filter issues updated before date (YYYY-MM-DD or RFC3339)")
|
||||
listCmd.Flags().String("closed-after", "", "Filter issues closed after date (YYYY-MM-DD or RFC3339)")
|
||||
listCmd.Flags().String("closed-before", "", "Filter issues closed before date (YYYY-MM-DD or RFC3339)")
|
||||
|
||||
|
||||
// Empty/null checks
|
||||
listCmd.Flags().Bool("empty-description", false, "Filter issues with empty or missing description")
|
||||
listCmd.Flags().Bool("no-assignee", false, "Filter issues with no assignee")
|
||||
listCmd.Flags().Bool("no-labels", false, "Filter issues with no labels")
|
||||
|
||||
|
||||
// Priority ranges
|
||||
listCmd.Flags().String("priority-min", "", "Filter by minimum priority (inclusive, 0-4 or P0-P4)")
|
||||
listCmd.Flags().String("priority-max", "", "Filter by maximum priority (inclusive, 0-4 or P0-P4)")
|
||||
|
||||
@@ -139,7 +139,9 @@ func runChecks(jsonOutput bool) {
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
enc.Encode(result)
|
||||
if err := enc.Encode(result); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error encoding preflight result: %v\n", err)
|
||||
}
|
||||
} else {
|
||||
// Human-readable output
|
||||
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)
|
||||
// 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.
|
||||
// #nosec G201 - expressions are column names, not user input
|
||||
copySQL := fmt.Sprintf(`
|
||||
INSERT INTO issues_new (
|
||||
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)
|
||||
|
||||
// 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()
|
||||
if err != nil {
|
||||
// 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
|
||||
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()
|
||||
if err != nil {
|
||||
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
|
||||
// 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 {
|
||||
// Stored SHA is ancestor - normal update, no force-push
|
||||
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
|
||||
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()
|
||||
if err != nil {
|
||||
// Remote branch might not exist yet (first push)
|
||||
// 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()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get sync branch SHA: %w", err)
|
||||
|
||||
@@ -100,7 +100,7 @@ func ToPager(content string, opts PagerOptions) error {
|
||||
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.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
@@ -34,6 +34,17 @@ log_error() {
|
||||
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
|
||||
# See: https://github.com/steveyegge/beads/issues/466
|
||||
resign_for_macos() {
|
||||
@@ -70,6 +81,9 @@ detect_platform() {
|
||||
Linux)
|
||||
os="linux"
|
||||
;;
|
||||
FreeBSD)
|
||||
os="freebsd"
|
||||
;;
|
||||
*)
|
||||
log_error "Unsupported operating system: $(uname -s)"
|
||||
exit 1
|
||||
@@ -83,6 +97,9 @@ detect_platform() {
|
||||
aarch64|arm64)
|
||||
arch="arm64"
|
||||
;;
|
||||
armv7*|armv6*|armhf|arm)
|
||||
arch="arm"
|
||||
;;
|
||||
*)
|
||||
log_error "Unsupported architecture: $(uname -m)"
|
||||
exit 1
|
||||
@@ -104,16 +121,19 @@ install_from_release() {
|
||||
log_info "Fetching latest release..."
|
||||
local latest_url="https://api.github.com/repos/steveyegge/beads/releases/latest"
|
||||
local version
|
||||
|
||||
local release_json
|
||||
|
||||
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
|
||||
version=$(wget -qO- "$latest_url" | grep '"tag_name"' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/')
|
||||
release_json=$(wget -qO- "$latest_url")
|
||||
else
|
||||
log_error "Neither curl nor wget found. Please install one of them."
|
||||
return 1
|
||||
fi
|
||||
|
||||
version=$(echo "$release_json" | grep '"tag_name"' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/')
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
log_error "Failed to fetch latest version"
|
||||
return 1
|
||||
@@ -124,6 +144,12 @@ install_from_release() {
|
||||
# Download URL
|
||||
local archive_name="beads_${version#v}_${platform}.tar.gz"
|
||||
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..."
|
||||
|
||||
|
||||
Reference in New Issue
Block a user