diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..96d26bdd1a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,19 @@ +name: build +on: + workflow_dispatch: +jobs: + build: + strategy: + matrix: + os: [ubuntu-24.04, macos-14, windows-2022] + name: build ${{ matrix.os }} + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-go@v5 + with: + go-version: '1.25.0' + - name: install ./... + run: go build ./... + env: + CGO_ENABLED: "0" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d01f019db5..2c3d89fc5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,27 +5,25 @@ on: - main pull_request: jobs: - test: + build: strategy: matrix: - # Disabling windows builds while we fix installing PostgreSQL 16 - # os: [ubuntu-22.04, macos-14, windows-2022] - os: [ubuntu-22.04, macos-15] - cgo: ['1', '0'] - # Workaround no native support for conditional matrix items - # https://github.com/orgs/community/discussions/26253#discussioncomment-6745038 - isMain: - - ${{ github.ref == 'refs/heads/main' }} - exclude: - - isMain: false - include: - - os: ubuntu-22.04 - cgo: '1' - - os: ubuntu-22.04 - cgo: '0' - name: test ${{ matrix.os }} cgo=${{ matrix.cgo }} - runs-on: ${{ matrix.os }} - + goos: [darwin, linux, windows] + goarch: [amd64, arm64] + name: build ${{ matrix.goos }}/${{ matrix.goarch }} + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-go@v5 + with: + go-version: '1.25.0' + - run: go build ./... + env: + CGO_ENABLED: "0" + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + test: + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v5 - uses: actions/setup-go@v5 @@ -44,37 +42,25 @@ jobs: - name: install ./... run: go install ./... env: - CGO_ENABLED: ${{ matrix.cgo }} + CGO_ENABLED: "0" - name: build internal/endtoend run: go build ./... working-directory: internal/endtoend/testdata env: - CGO_ENABLED: ${{ matrix.cgo }} - - # Start a PostgreSQL server - - uses: sqlc-dev/action-setup-postgres@master - with: - postgres-version: "16" - id: postgres - - # Start a MySQL server - - uses: shogo82148/actions-setup-mysql@v1 - with: - mysql-version: "9.0" + CGO_ENABLED: "0" - name: test ./... run: gotestsum --junitfile junit.xml -- --tags=examples -timeout 20m ./... + if: ${{ matrix.os }} != "windows-2022" env: CI_SQLC_PROJECT_ID: ${{ secrets.CI_SQLC_PROJECT_ID }} CI_SQLC_AUTH_TOKEN: ${{ secrets.CI_SQLC_AUTH_TOKEN }} SQLC_AUTH_TOKEN: ${{ secrets.CI_SQLC_AUTH_TOKEN }} - MYSQL_SERVER_URI: root:@tcp(localhost:3306)/mysql?multiStatements=true&parseTime=true - POSTGRESQL_SERVER_URI: ${{ steps.postgres.outputs.connection-uri }}?sslmode=disable - CGO_ENABLED: ${{ matrix.cgo }} + CGO_ENABLED: "0" vuln_check: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 timeout-minutes: 5 steps: diff --git a/internal/endtoend/endtoend_test.go b/internal/endtoend/endtoend_test.go index da6d7a405a..311eba9825 100644 --- a/internal/endtoend/endtoend_test.go +++ b/internal/endtoend/endtoend_test.go @@ -17,7 +17,7 @@ import ( "github.com/sqlc-dev/sqlc/internal/cmd" "github.com/sqlc-dev/sqlc/internal/config" "github.com/sqlc-dev/sqlc/internal/opts" - "github.com/sqlc-dev/sqlc/internal/sqltest/local" + "github.com/sqlc-dev/sqlc/internal/sqltest/docker" ) func lineEndings() cmp.Option { @@ -112,6 +112,24 @@ func TestReplay(t *testing.T) { // t.Parallel() ctx := context.Background() + var mysqlURI, postgresURI string + if err := docker.Installed(); err == nil { + { + host, err := docker.StartPostgreSQLServer(ctx) + if err != nil { + t.Fatalf("starting postgresql failed: %s", err) + } + postgresURI = host + } + { + host, err := docker.StartMySQLServer(ctx) + if err != nil { + t.Fatalf("starting mysql failed: %s", err) + } + mysqlURI = host + } + } + contexts := map[string]textContext{ "base": { Mutate: func(t *testing.T, path string) func(*config.Config) { return func(c *config.Config) {} }, @@ -124,13 +142,13 @@ func TestReplay(t *testing.T) { { Name: "postgres", Engine: config.EnginePostgreSQL, - URI: local.PostgreSQLServer(), + URI: postgresURI, }, { Name: "mysql", Engine: config.EngineMySQL, - URI: local.MySQLServer(), + URI: mysqlURI, }, } for i := range c.SQL { @@ -150,13 +168,8 @@ func TestReplay(t *testing.T) { } }, Enabled: func() bool { - if len(os.Getenv("POSTGRESQL_SERVER_URI")) == 0 { - return false - } - if len(os.Getenv("MYSQL_SERVER_URI")) == 0 { - return false - } - return true + err := docker.Installed() + return err == nil }, }, } diff --git a/internal/sqltest/docker/enabled.go b/internal/sqltest/docker/enabled.go new file mode 100644 index 0000000000..e17c0201b2 --- /dev/null +++ b/internal/sqltest/docker/enabled.go @@ -0,0 +1,17 @@ +package docker + +import ( + "fmt" + "os/exec" + + "golang.org/x/sync/singleflight" +) + +var flight singleflight.Group + +func Installed() error { + if _, err := exec.LookPath("docker"); err != nil { + return fmt.Errorf("docker not found: %w", err) + } + return nil +} diff --git a/internal/sqltest/docker/mysql.go b/internal/sqltest/docker/mysql.go new file mode 100644 index 0000000000..39a1af6160 --- /dev/null +++ b/internal/sqltest/docker/mysql.go @@ -0,0 +1,104 @@ +package docker + +import ( + "context" + "database/sql" + "fmt" + "os/exec" + "strings" + "time" + + _ "github.com/go-sql-driver/mysql" +) + +var mysqlHost string + +func StartMySQLServer(c context.Context) (string, error) { + if err := Installed(); err != nil { + return "", err + } + if mysqlHost != "" { + return mysqlHost, nil + } + value, err, _ := flight.Do("mysql", func() (interface{}, error) { + host, err := startMySQLServer(c) + if err != nil { + return "", err + } + mysqlHost = host + return host, nil + }) + if err != nil { + return "", err + } + data, ok := value.(string) + if !ok { + return "", fmt.Errorf("returned value was not a string") + } + return data, nil +} + +func startMySQLServer(c context.Context) (string, error) { + { + _, err := exec.Command("docker", "pull", "mysql:9").CombinedOutput() + if err != nil { + return "", fmt.Errorf("docker pull: mysql:9 %w", err) + } + } + + var exists bool + { + cmd := exec.Command("docker", "container", "inspect", "sqlc_sqltest_docker_mysql") + // This means we've already started the container + exists = cmd.Run() == nil + } + + if !exists { + cmd := exec.Command("docker", "run", + "--name", "sqlc_sqltest_docker_mysql", + "-e", "MYSQL_ROOT_PASSWORD=mysecretpassword", + "-e", "MYSQL_DATABASE=dinotest", + "-p", "3306:3306", + "-d", + "mysql:9", + ) + + output, err := cmd.CombinedOutput() + fmt.Println(string(output)) + + msg := `Conflict. The container name "/sqlc_sqltest_docker_mysql" is already in use by container` + if !strings.Contains(string(output), msg) && err != nil { + return "", err + } + } + + ctx, cancel := context.WithTimeout(c, 10*time.Second) + defer cancel() + + // Create a ticker that fires every 10ms + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + + uri := "root:mysecretpassword@/dinotest?multiStatements=true&parseTime=true" + + db, err := sql.Open("mysql", uri) + if err != nil { + return "", fmt.Errorf("sql.Open: %w", err) + } + + defer db.Close() + + for { + select { + case <-ctx.Done(): + return "", fmt.Errorf("timeout reached: %w", ctx.Err()) + + case <-ticker.C: + // Run your function here + if err := db.PingContext(ctx); err != nil { + continue + } + return uri, nil + } + } +} diff --git a/internal/sqltest/docker/postgres.go b/internal/sqltest/docker/postgres.go new file mode 100644 index 0000000000..1b2d842c70 --- /dev/null +++ b/internal/sqltest/docker/postgres.go @@ -0,0 +1,105 @@ +package docker + +import ( + "context" + "fmt" + "log/slog" + "os/exec" + "strings" + "time" + + "github.com/jackc/pgx/v5" +) + +var postgresHost string + +func StartPostgreSQLServer(c context.Context) (string, error) { + if err := Installed(); err != nil { + return "", err + } + if postgresHost != "" { + return postgresHost, nil + } + value, err, _ := flight.Do("postgresql", func() (interface{}, error) { + host, err := startPostgreSQLServer(c) + if err != nil { + return "", err + } + postgresHost = host + return host, err + }) + if err != nil { + return "", err + } + data, ok := value.(string) + if !ok { + return "", fmt.Errorf("returned value was not a string") + } + return data, nil +} + +func startPostgreSQLServer(c context.Context) (string, error) { + { + _, err := exec.Command("docker", "pull", "postgres:16").CombinedOutput() + if err != nil { + return "", fmt.Errorf("docker pull: postgres:16 %w", err) + } + } + + uri := "postgres://postgres:mysecretpassword@localhost:5432/postgres?sslmode=disable" + + var exists bool + { + cmd := exec.Command("docker", "container", "inspect", "sqlc_sqltest_docker_postgres") + // This means we've already started the container + exists = cmd.Run() == nil + } + + if !exists { + cmd := exec.Command("docker", "run", + "--name", "sqlc_sqltest_docker_postgres", + "-e", "POSTGRES_PASSWORD=mysecretpassword", + "-e", "POSTGRES_USER=postgres", + "-p", "5432:5432", + "-d", + "postgres:16", + "-c", "max_connections=200", + ) + + output, err := cmd.CombinedOutput() + fmt.Println(string(output)) + + msg := `Conflict. The container name "/sqlc_sqltest_docker_postgres" is already in use by container` + if !strings.Contains(string(output), msg) && err != nil { + return "", err + } + } + + ctx, cancel := context.WithTimeout(c, 5*time.Second) + defer cancel() + + // Create a ticker that fires every 10ms + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return "", fmt.Errorf("timeout reached: %w", ctx.Err()) + + case <-ticker.C: + // Run your function here + conn, err := pgx.Connect(ctx, uri) + if err != nil { + slog.Debug("sqltest", "connect", err) + continue + } + defer conn.Close(ctx) + if err := conn.Ping(ctx); err != nil { + slog.Error("sqltest", "ping", err) + continue + } + return uri, nil + } + } +} diff --git a/internal/sqltest/local/mysql.go b/internal/sqltest/local/mysql.go index 9c068a39ba..dedd3dfd78 100644 --- a/internal/sqltest/local/mysql.go +++ b/internal/sqltest/local/mysql.go @@ -13,22 +13,27 @@ import ( migrate "github.com/sqlc-dev/sqlc/internal/migrations" "github.com/sqlc-dev/sqlc/internal/sql/sqlpath" + "github.com/sqlc-dev/sqlc/internal/sqltest/docker" ) var mysqlSync sync.Once var mysqlPool *sql.DB -func MySQLServer() string { - return os.Getenv("MYSQL_SERVER_URI") -} - func MySQL(t *testing.T, migrations []string) string { ctx := context.Background() t.Helper() dburi := os.Getenv("MYSQL_SERVER_URI") if dburi == "" { - t.Skip("MYSQL_SERVER_URI is empty") + if ierr := docker.Installed(); ierr == nil { + u, err := docker.StartMySQLServer(ctx) + if err != nil { + t.Fatal(err) + } + dburi = u + } else { + t.Skip("MYSQL_SERVER_URI is empty") + } } mysqlSync.Do(func() { diff --git a/internal/sqltest/local/postgres.go b/internal/sqltest/local/postgres.go index 7b2c16c40a..feda4cf7ac 100644 --- a/internal/sqltest/local/postgres.go +++ b/internal/sqltest/local/postgres.go @@ -15,6 +15,7 @@ import ( migrate "github.com/sqlc-dev/sqlc/internal/migrations" "github.com/sqlc-dev/sqlc/internal/pgx/poolcache" "github.com/sqlc-dev/sqlc/internal/sql/sqlpath" + "github.com/sqlc-dev/sqlc/internal/sqltest/docker" ) var flight singleflight.Group @@ -28,17 +29,21 @@ func ReadOnlyPostgreSQL(t *testing.T, migrations []string) string { return postgreSQL(t, migrations, false) } -func PostgreSQLServer() string { - return os.Getenv("POSTGRESQL_SERVER_URI") -} - func postgreSQL(t *testing.T, migrations []string, rw bool) string { ctx := context.Background() t.Helper() dburi := os.Getenv("POSTGRESQL_SERVER_URI") if dburi == "" { - t.Skip("POSTGRESQL_SERVER_URI is empty") + if ierr := docker.Installed(); ierr == nil { + u, err := docker.StartPostgreSQLServer(ctx) + if err != nil { + t.Fatal(err) + } + dburi = u + } else { + t.Skip("POSTGRESQL_SERVER_URI is empty") + } } postgresPool, err := cache.Open(ctx, dburi)