55 "database/sql"
66 "fmt"
77 "log/slog"
8- "os"
98 "os/exec"
109 "time"
1110
@@ -16,8 +15,9 @@ import (
1615var mysqlFlight singleflight.Group
1716var mysqlURI string
1817
19- // StartMySQLServer installs and starts MySQL natively (without Docker).
20- // This is intended for CI environments like GitHub Actions where Docker may not be available.
18+ // StartMySQLServer starts an existing MySQL installation natively (without Docker).
19+ // This is intended for CI environments like GitHub Actions where Docker may not be available
20+ // but MySQL can be installed via the services directive.
2121func StartMySQLServer (ctx context.Context ) (string , error ) {
2222 if err := Supported (); err != nil {
2323 return "" , err
@@ -44,7 +44,7 @@ func StartMySQLServer(ctx context.Context) (string, error) {
4444}
4545
4646func startMySQLServer (ctx context.Context ) (string , error ) {
47- // Standard URI for test MySQL
47+ // Standard URI for test MySQL (matches GitHub Actions MySQL service default)
4848 uri := "root:mysecretpassword@tcp(localhost:3306)/mysql?multiStatements=true&parseTime=true"
4949
5050 // Try to connect first - it might already be running
@@ -56,9 +56,12 @@ func startMySQLServer(ctx context.Context) (string, error) {
5656 // Also try without password (default MySQL installation)
5757 uriNoPassword := "root@tcp(localhost:3306)/mysql?multiStatements=true&parseTime=true"
5858 if err := waitForMySQL (ctx , uriNoPassword , 500 * time .Millisecond ); err == nil {
59- // MySQL is running without password, set one
59+ slog .Info ("native/mysql" , "status" , "already running (no password)" )
60+ // MySQL is running without password, try to set one
6061 if err := setMySQLPassword (ctx ); err != nil {
6162 slog .Debug ("native/mysql" , "set-password-error" , err )
63+ // Return without password if we can't set one
64+ return uriNoPassword , nil
6265 }
6366 // Try again with password
6467 if err := waitForMySQL (ctx , uri , 1 * time .Second ); err == nil {
@@ -68,103 +71,64 @@ func startMySQLServer(ctx context.Context) (string, error) {
6871 return uriNoPassword , nil
6972 }
7073
71- // Try to start existing MySQL service first (might be installed but not running)
74+ // Try to start existing MySQL service (might be installed but not running)
7275 if _ , err := exec .LookPath ("mysqld" ); err == nil {
7376 slog .Info ("native/mysql" , "status" , "starting existing service" )
74- if err := startMySQLService (); err == nil {
77+ if err := startMySQLService (); err != nil {
78+ slog .Debug ("native/mysql" , "start-error" , err )
79+ } else {
7580 // Wait for MySQL to be ready
7681 waitCtx , cancel := context .WithTimeout (ctx , 30 * time .Second )
7782 defer cancel ()
7883
79- // Try without password first
80- if err := waitForMySQL (waitCtx , uriNoPassword , 30 * time .Second ); err == nil {
84+ // Try with password first (GitHub Actions MySQL service has password)
85+ if err := waitForMySQL (waitCtx , uri , 15 * time .Second ); err == nil {
86+ return uri , nil
87+ }
88+
89+ // Try without password
90+ if err := waitForMySQL (waitCtx , uriNoPassword , 15 * time .Second ); err == nil {
8191 if err := setMySQLPassword (ctx ); err != nil {
8292 slog .Debug ("native/mysql" , "set-password-error" , err )
8393 return uriNoPassword , nil
8494 }
85- return uri , nil
86- }
87-
88- // Try with password
89- if err := waitForMySQL (waitCtx , uri , 5 * time .Second ); err == nil {
90- return uri , nil
95+ if err := waitForMySQL (ctx , uri , 1 * time .Second ); err == nil {
96+ return uri , nil
97+ }
98+ return uriNoPassword , nil
9199 }
92100 }
93101 }
94102
95- // Install MySQL if needed
96- if _ , err := exec .LookPath ("mysql" ); err != nil {
97- slog .Info ("native/mysql" , "status" , "installing" )
98-
99- // Pre-configure MySQL root password (with timeout using Linux timeout command)
100- setSelectionsCmd := exec .Command ("sudo" , "timeout" , "10" ,
101- "bash" , "-c" ,
102- `echo "mysql-server mysql-server/root_password password mysecretpassword" | sudo debconf-set-selections && ` +
103- `echo "mysql-server mysql-server/root_password_again password mysecretpassword" | sudo debconf-set-selections` )
104- setSelectionsCmd .Env = append (os .Environ (), "DEBIAN_FRONTEND=noninteractive" )
105- if output , err := setSelectionsCmd .CombinedOutput (); err != nil {
106- slog .Debug ("native/mysql" , "debconf" , string (output ))
107- }
108-
109- // Try to install MySQL server (with 60 second timeout using Linux timeout command)
110- cmd := exec .Command ("sudo" , "timeout" , "60" , "apt-get" , "install" , "-y" , "-qq" , "mysql-server" )
111- cmd .Env = append (os .Environ (), "DEBIAN_FRONTEND=noninteractive" )
112- if output , err := cmd .CombinedOutput (); err != nil {
113- // If apt-get fails (no network or timeout), return error
114- return "" , fmt .Errorf ("apt-get install mysql-server failed (network may be unavailable or timed out): %w\n %s" , err , output )
115- }
116- }
117-
118- // Start MySQL service
119- slog .Info ("native/mysql" , "status" , "starting service" )
120- if err := startMySQLService (); err != nil {
121- return "" , fmt .Errorf ("failed to start MySQL: %w" , err )
122- }
123-
124- // Wait for MySQL to be ready with no password first
125- waitCtx , cancel := context .WithTimeout (ctx , 30 * time .Second )
126- defer cancel ()
127-
128- // Try without password first (fresh installation)
129- if err := waitForMySQL (waitCtx , uriNoPassword , 30 * time .Second ); err == nil {
130- // Set the password
131- if err := setMySQLPassword (ctx ); err != nil {
132- slog .Debug ("native/mysql" , "set-password-error" , err )
133- // Return without password
134- return uriNoPassword , nil
135- }
136- return uri , nil
137- }
138-
139- // Try with password
140- if err := waitForMySQL (waitCtx , uri , 5 * time .Second ); err != nil {
141- return "" , fmt .Errorf ("timeout waiting for MySQL: %w" , err )
142- }
143-
144- return uri , nil
103+ return "" , fmt .Errorf ("MySQL is not installed or could not be started" )
145104}
146105
147106func startMySQLService () error {
148107 // Try systemctl first
149108 cmd := exec .Command ("sudo" , "systemctl" , "start" , "mysql" )
150109 if err := cmd .Run (); err == nil {
110+ // Give MySQL time to fully initialize
111+ time .Sleep (2 * time .Second )
151112 return nil
152113 }
153114
154115 // Try mysqld
155116 cmd = exec .Command ("sudo" , "systemctl" , "start" , "mysqld" )
156117 if err := cmd .Run (); err == nil {
118+ time .Sleep (2 * time .Second )
157119 return nil
158120 }
159121
160122 // Try service command
161123 cmd = exec .Command ("sudo" , "service" , "mysql" , "start" )
162124 if err := cmd .Run (); err == nil {
125+ time .Sleep (2 * time .Second )
163126 return nil
164127 }
165128
166129 cmd = exec .Command ("sudo" , "service" , "mysqld" , "start" )
167130 if err := cmd .Run (); err == nil {
131+ time .Sleep (2 * time .Second )
168132 return nil
169133 }
170134
@@ -179,28 +143,29 @@ func setMySQLPassword(ctx context.Context) error {
179143 }
180144 defer db .Close ()
181145
182- // Set root password
183- _ , err = db .ExecContext (ctx , "ALTER USER 'root'@'localhost' IDENTIFIED BY 'mysecretpassword';" )
146+ // Set root password using mysql_native_password for broader compatibility
147+ _ , err = db .ExecContext (ctx , "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'mysecretpassword';" )
184148 if err != nil {
185- // Try older MySQL syntax
186- _ , err = db .ExecContext (ctx , "SET PASSWORD FOR 'root'@'localhost' = PASSWORD( 'mysecretpassword') ;" )
149+ // Try without specifying auth plugin
150+ _ , err = db .ExecContext (ctx , "ALTER USER 'root'@'localhost' IDENTIFIED BY 'mysecretpassword';" )
187151 if err != nil {
188- return fmt .Errorf ("could not set MySQL password: %w" , err )
152+ // Try older MySQL syntax
153+ _ , err = db .ExecContext (ctx , "SET PASSWORD FOR 'root'@'localhost' = PASSWORD('mysecretpassword');" )
154+ if err != nil {
155+ return fmt .Errorf ("could not set MySQL password: %w" , err )
156+ }
189157 }
190158 }
191159
192160 // Flush privileges
193161 _ , _ = db .ExecContext (ctx , "FLUSH PRIVILEGES;" )
194162
195- // Create dinotest database
196- _ , _ = db .ExecContext (ctx , "CREATE DATABASE IF NOT EXISTS dinotest;" )
197-
198163 return nil
199164}
200165
201166func waitForMySQL (ctx context.Context , uri string , timeout time.Duration ) error {
202167 deadline := time .Now ().Add (timeout )
203- ticker := time .NewTicker (100 * time .Millisecond )
168+ ticker := time .NewTicker (500 * time .Millisecond )
204169 defer ticker .Stop ()
205170
206171 var lastErr error
@@ -218,7 +183,11 @@ func waitForMySQL(ctx context.Context, uri string, timeout time.Duration) error
218183 slog .Debug ("native/mysql" , "open-attempt" , err )
219184 continue
220185 }
221- if err := db .PingContext (ctx ); err != nil {
186+ // Use a short timeout for ping to avoid hanging
187+ pingCtx , cancel := context .WithTimeout (ctx , 2 * time .Second )
188+ err = db .PingContext (pingCtx )
189+ cancel ()
190+ if err != nil {
222191 lastErr = err
223192 db .Close ()
224193 continue
0 commit comments