ScuttleBot

fix: ergo binary download and config generation on macOS Three bugs prevented scuttlebot from starting: 1. ergo release assets use "macos" not "darwin" in filenames; platformSuffix() now maps darwin → macos 2. EnsureBinary returns a relative path; when the manager sets cmd.Dir to the data dir, Go resolves the binary path relative to that dir (double-nesting). Now resolved to absolute in main. 3. configPath() returned a relative path (data/ergo/ircd.yaml); same CWD issue — ergo looked for data/ergo/data/ergo/ircd.yaml. Now resolved to absolute. Also fixed ircd.yaml template: - max-sendq belongs under server:, not limits: - datastore.path changed to ./ircd.db (relative to ergo's CWD) - removed opers/oper-classes (bots use SASL, not /OPER)

lmata 2026-03-31 12:51 trunk
Commit c8d931091a33069958072f3606cf455d0ba6b5dcca28b3c9ed44f0bb0a648913
--- cmd/scuttlebot/main.go
+++ cmd/scuttlebot/main.go
@@ -8,10 +8,11 @@
88
"fmt"
99
"log/slog"
1010
"net/http"
1111
"os"
1212
"os/signal"
13
+ "path/filepath"
1314
"syscall"
1415
"time"
1516
1617
"github.com/conflicthq/scuttlebot/internal/api"
1718
"github.com/conflicthq/scuttlebot/internal/config"
@@ -41,11 +42,16 @@
4142
binary, err := ergo.EnsureBinary(cfg.Ergo.BinaryPath, cfg.Ergo.DataDir)
4243
if err != nil {
4344
log.Error("ergo binary unavailable", "err", err)
4445
os.Exit(1)
4546
}
46
- cfg.Ergo.BinaryPath = binary
47
+ abs, err := filepath.Abs(binary)
48
+ if err != nil {
49
+ log.Error("resolve ergo binary path", "err", err)
50
+ os.Exit(1)
51
+ }
52
+ cfg.Ergo.BinaryPath = abs
4753
}
4854
4955
// Generate an API token for the Ergo management API if not set.
5056
if cfg.Ergo.APIToken == "" {
5157
cfg.Ergo.APIToken = mustGenToken()
5258
--- cmd/scuttlebot/main.go
+++ cmd/scuttlebot/main.go
@@ -8,10 +8,11 @@
8 "fmt"
9 "log/slog"
10 "net/http"
11 "os"
12 "os/signal"
 
13 "syscall"
14 "time"
15
16 "github.com/conflicthq/scuttlebot/internal/api"
17 "github.com/conflicthq/scuttlebot/internal/config"
@@ -41,11 +42,16 @@
41 binary, err := ergo.EnsureBinary(cfg.Ergo.BinaryPath, cfg.Ergo.DataDir)
42 if err != nil {
43 log.Error("ergo binary unavailable", "err", err)
44 os.Exit(1)
45 }
46 cfg.Ergo.BinaryPath = binary
 
 
 
 
 
47 }
48
49 // Generate an API token for the Ergo management API if not set.
50 if cfg.Ergo.APIToken == "" {
51 cfg.Ergo.APIToken = mustGenToken()
52
--- cmd/scuttlebot/main.go
+++ cmd/scuttlebot/main.go
@@ -8,10 +8,11 @@
8 "fmt"
9 "log/slog"
10 "net/http"
11 "os"
12 "os/signal"
13 "path/filepath"
14 "syscall"
15 "time"
16
17 "github.com/conflicthq/scuttlebot/internal/api"
18 "github.com/conflicthq/scuttlebot/internal/config"
@@ -41,11 +42,16 @@
42 binary, err := ergo.EnsureBinary(cfg.Ergo.BinaryPath, cfg.Ergo.DataDir)
43 if err != nil {
44 log.Error("ergo binary unavailable", "err", err)
45 os.Exit(1)
46 }
47 abs, err := filepath.Abs(binary)
48 if err != nil {
49 log.Error("resolve ergo binary path", "err", err)
50 os.Exit(1)
51 }
52 cfg.Ergo.BinaryPath = abs
53 }
54
55 // Generate an API token for the Ergo management API if not set.
56 if cfg.Ergo.APIToken == "" {
57 cfg.Ergo.APIToken = mustGenToken()
58
--- internal/ergo/fetch.go
+++ internal/ergo/fetch.go
@@ -83,17 +83,21 @@
8383
8484
return "", "", fmt.Errorf("no release asset found for %s/%s (tag %s)", runtime.GOOS, runtime.GOARCH, release.TagName)
8585
}
8686
8787
// platformSuffix returns the OS-arch suffix used in ergo release filenames.
88
+// Ergo uses "macos" instead of "darwin" and "x86_64" instead of "amd64".
8889
func platformSuffix() string {
89
- os := runtime.GOOS
90
+ goos := runtime.GOOS
91
+ if goos == "darwin" {
92
+ goos = "macos"
93
+ }
9094
arch := runtime.GOARCH
9195
if arch == "amd64" {
9296
arch = "x86_64"
9397
}
94
- return os + "-" + arch
98
+ return goos + "-" + arch
9599
}
96100
97101
func matchesPlatform(name, suffix string) bool {
98102
// Ergo assets look like: ergo-v2.14.0-linux-x86_64.tar.gz
99103
return len(name) > 0 &&
100104
--- internal/ergo/fetch.go
+++ internal/ergo/fetch.go
@@ -83,17 +83,21 @@
83
84 return "", "", fmt.Errorf("no release asset found for %s/%s (tag %s)", runtime.GOOS, runtime.GOARCH, release.TagName)
85 }
86
87 // platformSuffix returns the OS-arch suffix used in ergo release filenames.
 
88 func platformSuffix() string {
89 os := runtime.GOOS
 
 
 
90 arch := runtime.GOARCH
91 if arch == "amd64" {
92 arch = "x86_64"
93 }
94 return os + "-" + arch
95 }
96
97 func matchesPlatform(name, suffix string) bool {
98 // Ergo assets look like: ergo-v2.14.0-linux-x86_64.tar.gz
99 return len(name) > 0 &&
100
--- internal/ergo/fetch.go
+++ internal/ergo/fetch.go
@@ -83,17 +83,21 @@
83
84 return "", "", fmt.Errorf("no release asset found for %s/%s (tag %s)", runtime.GOOS, runtime.GOARCH, release.TagName)
85 }
86
87 // platformSuffix returns the OS-arch suffix used in ergo release filenames.
88 // Ergo uses "macos" instead of "darwin" and "x86_64" instead of "amd64".
89 func platformSuffix() string {
90 goos := runtime.GOOS
91 if goos == "darwin" {
92 goos = "macos"
93 }
94 arch := runtime.GOARCH
95 if arch == "amd64" {
96 arch = "x86_64"
97 }
98 return goos + "-" + arch
99 }
100
101 func matchesPlatform(name, suffix string) bool {
102 // Ergo assets look like: ergo-v2.14.0-linux-x86_64.tar.gz
103 return len(name) > 0 &&
104
--- internal/ergo/ircdconfig.go
+++ internal/ergo/ircdconfig.go
@@ -15,18 +15,19 @@
1515
name: {{.ServerName}}
1616
listeners:
1717
"{{.IRCAddr}}": {}
1818
casemapping: ascii
1919
enforce-utf8: true
20
+ max-sendq: 96k
2021
relaymsg:
2122
enabled: false
2223
ip-cloaking:
2324
enabled: false
2425
lookup-hostnames: false
2526
2627
datastore:
27
- path: {{.DataDir}}/ircd.db
28
+ path: ./ircd.db
2829
autoupgrade: true
2930
{{- if .HistoryEnabled}}
3031
{{- if .PostgresDSN}}
3132
postgresql:
3233
enabled: true
@@ -91,43 +92,10 @@
9192
channellen: 64
9293
awaylen: 200
9394
kicklen: 255
9495
topiclen: 512
9596
96
-opers:
97
- scuttlebot:
98
- class: ircop
99
- hidden: true
100
- whois-line: scuttlebot operator
101
-
102
-oper-classes:
103
- ircop:
104
- title: IRC Operator
105
- capabilities:
106
- - oper:local_kill
107
- - oper:local_ban
108
- - oper:local_unban
109
- - oper:remote_kill
110
- - oper:remote_ban
111
- - oper:remote_unban
112
- - oper:rehash
113
- - oper:die
114
- - oper:nofakelag
115
- - oper:relaymsg
116
- - ban:nick
117
- - ban:hostname
118
- - ban:cidr
119
- - ban:ip
120
- - samode
121
- - sajoin
122
- - snomask
123
- - vhosts
124
- - accreg
125
- - chanreg
126
- - history
127
- - defcon
128
- - massmessage
12997
`))
13098
13199
type ircdTemplateData struct {
132100
NetworkName string
133101
ServerName string
134102
--- internal/ergo/ircdconfig.go
+++ internal/ergo/ircdconfig.go
@@ -15,18 +15,19 @@
15 name: {{.ServerName}}
16 listeners:
17 "{{.IRCAddr}}": {}
18 casemapping: ascii
19 enforce-utf8: true
 
20 relaymsg:
21 enabled: false
22 ip-cloaking:
23 enabled: false
24 lookup-hostnames: false
25
26 datastore:
27 path: {{.DataDir}}/ircd.db
28 autoupgrade: true
29 {{- if .HistoryEnabled}}
30 {{- if .PostgresDSN}}
31 postgresql:
32 enabled: true
@@ -91,43 +92,10 @@
91 channellen: 64
92 awaylen: 200
93 kicklen: 255
94 topiclen: 512
95
96 opers:
97 scuttlebot:
98 class: ircop
99 hidden: true
100 whois-line: scuttlebot operator
101
102 oper-classes:
103 ircop:
104 title: IRC Operator
105 capabilities:
106 - oper:local_kill
107 - oper:local_ban
108 - oper:local_unban
109 - oper:remote_kill
110 - oper:remote_ban
111 - oper:remote_unban
112 - oper:rehash
113 - oper:die
114 - oper:nofakelag
115 - oper:relaymsg
116 - ban:nick
117 - ban:hostname
118 - ban:cidr
119 - ban:ip
120 - samode
121 - sajoin
122 - snomask
123 - vhosts
124 - accreg
125 - chanreg
126 - history
127 - defcon
128 - massmessage
129 `))
130
131 type ircdTemplateData struct {
132 NetworkName string
133 ServerName string
134
--- internal/ergo/ircdconfig.go
+++ internal/ergo/ircdconfig.go
@@ -15,18 +15,19 @@
15 name: {{.ServerName}}
16 listeners:
17 "{{.IRCAddr}}": {}
18 casemapping: ascii
19 enforce-utf8: true
20 max-sendq: 96k
21 relaymsg:
22 enabled: false
23 ip-cloaking:
24 enabled: false
25 lookup-hostnames: false
26
27 datastore:
28 path: ./ircd.db
29 autoupgrade: true
30 {{- if .HistoryEnabled}}
31 {{- if .PostgresDSN}}
32 postgresql:
33 enabled: true
@@ -91,43 +92,10 @@
92 channellen: 64
93 awaylen: 200
94 kicklen: 255
95 topiclen: 512
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97 `))
98
99 type ircdTemplateData struct {
100 NetworkName string
101 ServerName string
102
--- internal/ergo/manager.go
+++ internal/ergo/manager.go
@@ -136,11 +136,15 @@
136136
}
137137
return os.WriteFile(m.configPath(), data, 0o600)
138138
}
139139
140140
func (m *Manager) configPath() string {
141
- return filepath.Join(m.cfg.DataDir, ircdConfigFile)
141
+ p := filepath.Join(m.cfg.DataDir, ircdConfigFile)
142
+ if abs, err := filepath.Abs(p); err == nil {
143
+ return abs
144
+ }
145
+ return p
142146
}
143147
144148
func (m *Manager) waitHealthy(ctx context.Context) error {
145149
deadline := time.Now().Add(healthTimeout)
146150
for time.Now().Before(deadline) {
147151
--- internal/ergo/manager.go
+++ internal/ergo/manager.go
@@ -136,11 +136,15 @@
136 }
137 return os.WriteFile(m.configPath(), data, 0o600)
138 }
139
140 func (m *Manager) configPath() string {
141 return filepath.Join(m.cfg.DataDir, ircdConfigFile)
 
 
 
 
142 }
143
144 func (m *Manager) waitHealthy(ctx context.Context) error {
145 deadline := time.Now().Add(healthTimeout)
146 for time.Now().Before(deadline) {
147
--- internal/ergo/manager.go
+++ internal/ergo/manager.go
@@ -136,11 +136,15 @@
136 }
137 return os.WriteFile(m.configPath(), data, 0o600)
138 }
139
140 func (m *Manager) configPath() string {
141 p := filepath.Join(m.cfg.DataDir, ircdConfigFile)
142 if abs, err := filepath.Abs(p); err == nil {
143 return abs
144 }
145 return p
146 }
147
148 func (m *Manager) waitHealthy(ctx context.Context) error {
149 deadline := time.Now().Add(healthTimeout)
150 for time.Now().Before(deadline) {
151

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button