From bd8e1b3477390448e1d8b34553f2c0f613eac537 Mon Sep 17 00:00:00 2001 From: whywaita Date: Thu, 20 Aug 2026 14:21:07 +0900 Subject: [PATCH 1/5] refactor: migrate uuid to Go 1.27 standard library Replace github.com/satori/go.uuid with the standard library uuid package added in Go 1.27. - uuid.FromString -> uuid.Parse - uuid.FromStringOrNil -> uuid.MustParse - uuid.Equal(a, b) -> a == b (UUID is a comparable [16]byte) - uuid.NewV4 / uuid.UUID / uuid.UUID{} map 1:1 Bump go directive 1.25 -> 1.27 and drop the satori/go.uuid module. --- go.mod | 3 +-- go.sum | 2 -- pkg/datastore/interface.go | 2 +- pkg/datastore/memory/memory.go | 4 ++-- pkg/datastore/mysql/job.go | 2 +- pkg/datastore/mysql/job_test.go | 4 ++-- pkg/datastore/mysql/runner.go | 2 +- pkg/datastore/mysql/runner_test.go | 14 +++++++------- pkg/datastore/mysql/target.go | 2 +- pkg/datastore/mysql/target_test.go | 8 ++++---- pkg/metric/scrape_memory.go | 2 +- pkg/runner/util.go | 4 ++-- pkg/starter/starter.go | 2 +- pkg/web/target.go | 4 ++-- pkg/web/target_create.go | 2 +- pkg/web/target_test.go | 2 +- pkg/web/webhook.go | 2 +- 17 files changed, 29 insertions(+), 32 deletions(-) diff --git a/go.mod b/go.mod index a9f0674..5d741cd 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/whywaita/myshoes -go 1.25 +go 1.27 require ( github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 @@ -16,7 +16,6 @@ require ( github.com/patrickmn/go-cache v2.1.0+incompatible github.com/prometheus/client_golang v1.23.2 github.com/r3labs/diff/v2 v2.15.1 - github.com/satori/go.uuid v1.2.0 goji.io v2.0.2+incompatible golang.org/x/oauth2 v0.32.0 golang.org/x/sync v0.18.0 diff --git a/go.sum b/go.sum index 0ae1540..a71cc14 100644 --- a/go.sum +++ b/go.sum @@ -145,8 +145,6 @@ github.com/r3labs/diff/v2 v2.15.1 h1:EOrVqPUzi+njlumoqJwiS/TgGgmZo83619FNDB9xQUg github.com/r3labs/diff/v2 v2.15.1/go.mod h1:I8noH9Fc2fjSaMxqF3G2lhDdC0b+JXCfyx85tWFM9kc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/pkg/datastore/interface.go b/pkg/datastore/interface.go index 39b0ccc..84843e1 100644 --- a/pkg/datastore/interface.go +++ b/pkg/datastore/interface.go @@ -9,7 +9,7 @@ import ( "strings" "time" - uuid "github.com/satori/go.uuid" + "uuid" "github.com/whywaita/myshoes/pkg/gh" "github.com/whywaita/myshoes/pkg/logger" diff --git a/pkg/datastore/memory/memory.go b/pkg/datastore/memory/memory.go index 497d585..5a4cccf 100644 --- a/pkg/datastore/memory/memory.go +++ b/pkg/datastore/memory/memory.go @@ -7,7 +7,7 @@ import ( "sync" "time" - uuid "github.com/satori/go.uuid" + "uuid" "github.com/whywaita/myshoes/pkg/datastore" ) @@ -214,7 +214,7 @@ func (m *Memory) ListRunnersByTargetID(ctx context.Context, targetID uuid.UUID) var runners []datastore.Runner for _, r := range m.runners { - if uuid.Equal(r.TargetID, targetID) { + if r.TargetID == targetID { runners = append(runners, r) } } diff --git a/pkg/datastore/mysql/job.go b/pkg/datastore/mysql/job.go index 58d757d..92a778f 100644 --- a/pkg/datastore/mysql/job.go +++ b/pkg/datastore/mysql/job.go @@ -6,8 +6,8 @@ import ( "errors" "fmt" - uuid "github.com/satori/go.uuid" "github.com/whywaita/myshoes/pkg/datastore" + "uuid" ) // EnqueueJob add a job diff --git a/pkg/datastore/mysql/job_test.go b/pkg/datastore/mysql/job_test.go index d98c60f..aef7492 100644 --- a/pkg/datastore/mysql/job_test.go +++ b/pkg/datastore/mysql/job_test.go @@ -10,13 +10,13 @@ import ( "github.com/google/go-cmp/cmp" "github.com/jmoiron/sqlx" - uuid "github.com/satori/go.uuid" + "uuid" "github.com/whywaita/myshoes/internal/testutils" "github.com/whywaita/myshoes/pkg/datastore" ) -var testJobID = uuid.FromStringOrNil("1b4e5b7a-e3c1-4829-9cfd-eac4183f2c95") +var testJobID = uuid.MustParse("1b4e5b7a-e3c1-4829-9cfd-eac4183f2c95") func TestMySQL_EnqueueJob(t *testing.T) { testDatastore, teardown := testutils.GetTestDatastore() diff --git a/pkg/datastore/mysql/runner.go b/pkg/datastore/mysql/runner.go index b6761f4..5dc8c33 100644 --- a/pkg/datastore/mysql/runner.go +++ b/pkg/datastore/mysql/runner.go @@ -7,8 +7,8 @@ import ( "fmt" "time" - uuid "github.com/satori/go.uuid" "github.com/whywaita/myshoes/pkg/datastore" + "uuid" ) // CreateRunner add a runner diff --git a/pkg/datastore/mysql/runner_test.go b/pkg/datastore/mysql/runner_test.go index d796aa3..3816961 100644 --- a/pkg/datastore/mysql/runner_test.go +++ b/pkg/datastore/mysql/runner_test.go @@ -11,12 +11,12 @@ import ( "github.com/google/go-cmp/cmp" "github.com/jmoiron/sqlx" - uuid "github.com/satori/go.uuid" "github.com/whywaita/myshoes/internal/testutils" "github.com/whywaita/myshoes/pkg/datastore" + "uuid" ) -var testRunnerID = uuid.FromStringOrNil("7943e412-c0ae-4068-ab24-3e71a13fbe53") +var testRunnerID = uuid.MustParse("7943e412-c0ae-4068-ab24-3e71a13fbe53") func TestMySQL_CreateRunner(t *testing.T) { testDatastore, teardown := testutils.GetTestDatastore() @@ -218,14 +218,14 @@ func TestMySQL_ListRunnersNotReturnDeleted(t *testing.T) { RepositoryURL: "https://github.com/octocat/Hello-World", RequestWebhook: "{}", } - input.UUID = uuid.FromStringOrNil(fmt.Sprintf(u, i)) + input.UUID = uuid.MustParse(fmt.Sprintf(u, i)) err := testDatastore.CreateRunner(context.Background(), input) if err != nil { t.Fatalf("failed to create runner: %+v", err) } } - err := testDatastore.DeleteRunner(context.Background(), uuid.FromStringOrNil(fmt.Sprintf(u, 0)), time.Now(), "deleted") + err := testDatastore.DeleteRunner(context.Background(), uuid.MustParse(fmt.Sprintf(u, 0)), time.Now(), "deleted") if err != nil { t.Fatalf("failed to delete runner: %+v", err) } @@ -250,7 +250,7 @@ func TestMySQL_ListRunnersNotReturnDeleted(t *testing.T) { RepositoryURL: "https://github.com/octocat/Hello-World", RequestWebhook: "{}", } - r.UUID = uuid.FromStringOrNil(fmt.Sprintf(u, i)) + r.UUID = uuid.MustParse(fmt.Sprintf(u, i)) want = append(want, r) } @@ -285,7 +285,7 @@ func TestMySQL_ListRunnersLogBySince(t *testing.T) { RepositoryURL: "https://github.com/octocat/Hello-World", RequestWebhook: "{}", } - input.UUID = uuid.FromStringOrNil(fmt.Sprintf(u, i)) + input.UUID = uuid.MustParse(fmt.Sprintf(u, i)) err := testDatastore.CreateRunner(context.Background(), input) if err != nil { t.Fatalf("failed to create runner: %+v", err) @@ -314,7 +314,7 @@ func TestMySQL_ListRunnersLogBySince(t *testing.T) { RepositoryURL: "https://github.com/octocat/Hello-World", RequestWebhook: "{}", } - r.UUID = uuid.FromStringOrNil(fmt.Sprintf(u, i)) + r.UUID = uuid.MustParse(fmt.Sprintf(u, i)) want = append(want, r) } diff --git a/pkg/datastore/mysql/target.go b/pkg/datastore/mysql/target.go index 4a0a5a2..0195b7e 100644 --- a/pkg/datastore/mysql/target.go +++ b/pkg/datastore/mysql/target.go @@ -7,8 +7,8 @@ import ( "fmt" "time" - uuid "github.com/satori/go.uuid" "github.com/whywaita/myshoes/pkg/datastore" + "uuid" ) // CreateTarget create a target diff --git a/pkg/datastore/mysql/target_test.go b/pkg/datastore/mysql/target_test.go index f828b0b..75e3f44 100644 --- a/pkg/datastore/mysql/target_test.go +++ b/pkg/datastore/mysql/target_test.go @@ -11,15 +11,15 @@ import ( "github.com/google/go-cmp/cmp" "github.com/jmoiron/sqlx" - uuid "github.com/satori/go.uuid" + "uuid" "github.com/whywaita/myshoes/internal/testutils" "github.com/whywaita/myshoes/pkg/datastore" ) -var testTargetID = uuid.FromStringOrNil("8a72d42c-372c-4e0d-9c6a-4304d44af137") -var testTargetID2 = uuid.FromStringOrNil("d14ccfea-b123-4ada-974e-bbff0937e9c7") -var testTargetID3 = uuid.FromStringOrNil("5c1816ff-4813-46b0-b1ba-30d135a2f3f5") +var testTargetID = uuid.MustParse("8a72d42c-372c-4e0d-9c6a-4304d44af137") +var testTargetID2 = uuid.MustParse("d14ccfea-b123-4ada-974e-bbff0937e9c7") +var testTargetID3 = uuid.MustParse("5c1816ff-4813-46b0-b1ba-30d135a2f3f5") var testScopeOrg = "octocat" var testScopeRepo = "octocat/hello-world" var testScopeRepo2 = "octocat/hello-world2" diff --git a/pkg/metric/scrape_memory.go b/pkg/metric/scrape_memory.go index d4908e4..20ac3a3 100644 --- a/pkg/metric/scrape_memory.go +++ b/pkg/metric/scrape_memory.go @@ -5,7 +5,7 @@ import ( "fmt" "sync/atomic" - uuid "github.com/satori/go.uuid" + "uuid" "github.com/prometheus/client_golang/prometheus" "github.com/whywaita/myshoes/pkg/config" diff --git a/pkg/runner/util.go b/pkg/runner/util.go index 35ba490..1c1fed2 100644 --- a/pkg/runner/util.go +++ b/pkg/runner/util.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - uuid "github.com/satori/go.uuid" "github.com/whywaita/myshoes/pkg/datastore" + "uuid" ) // ToName convert uuid to runner name @@ -16,7 +16,7 @@ func ToName(u string) string { // ToUUID convert runner name to uuid func ToUUID(name string) (uuid.UUID, error) { u := strings.TrimPrefix(name, "myshoes-") - return uuid.FromString(u) + return uuid.Parse(u) } // ToReason convert status from GitHub to datastore.RunnerStatus diff --git a/pkg/starter/starter.go b/pkg/starter/starter.go index 2c92675..fff6324 100644 --- a/pkg/starter/starter.go +++ b/pkg/starter/starter.go @@ -18,7 +18,6 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - uuid "github.com/satori/go.uuid" "github.com/whywaita/myshoes/internal/util" "github.com/whywaita/myshoes/pkg/config" "github.com/whywaita/myshoes/pkg/datastore" @@ -27,6 +26,7 @@ import ( "github.com/whywaita/myshoes/pkg/runner" "github.com/whywaita/myshoes/pkg/shoes" "github.com/whywaita/myshoes/pkg/starter/safety" + "uuid" ) var ( diff --git a/pkg/web/target.go b/pkg/web/target.go index 7c8ea44..f6e16c5 100644 --- a/pkg/web/target.go +++ b/pkg/web/target.go @@ -10,7 +10,7 @@ import ( "time" "github.com/r3labs/diff/v2" - uuid "github.com/satori/go.uuid" + "uuid" "github.com/whywaita/myshoes/pkg/datastore" "github.com/whywaita/myshoes/pkg/gh" @@ -221,7 +221,7 @@ func handleTargetDelete(w http.ResponseWriter, r *http.Request, ds datastore.Dat func parseReqTargetID(r *http.Request) (uuid.UUID, error) { targetIDStr := pat.Param(r, "id") - targetID, err := uuid.FromString(targetIDStr) + targetID, err := uuid.Parse(targetIDStr) if err != nil { return uuid.UUID{}, fmt.Errorf("failed to parse target id: %w", err) } diff --git a/pkg/web/target_create.go b/pkg/web/target_create.go index 6ed0353..d7c7942 100644 --- a/pkg/web/target_create.go +++ b/pkg/web/target_create.go @@ -9,7 +9,7 @@ import ( "net/http" "time" - uuid "github.com/satori/go.uuid" + "uuid" "github.com/whywaita/myshoes/pkg/config" "github.com/whywaita/myshoes/pkg/datastore" diff --git a/pkg/web/target_test.go b/pkg/web/target_test.go index 0f2baaa..3dd189c 100644 --- a/pkg/web/target_test.go +++ b/pkg/web/target_test.go @@ -13,7 +13,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-github/v80/github" - uuid "github.com/satori/go.uuid" + "uuid" "github.com/whywaita/myshoes/internal/testutils" "github.com/whywaita/myshoes/pkg/datastore" diff --git a/pkg/web/webhook.go b/pkg/web/webhook.go index e221eeb..f4047e5 100644 --- a/pkg/web/webhook.go +++ b/pkg/web/webhook.go @@ -11,7 +11,7 @@ import ( "time" "github.com/google/go-github/v80/github" - uuid "github.com/satori/go.uuid" + "uuid" "github.com/whywaita/myshoes/pkg/config" "github.com/whywaita/myshoes/pkg/datastore" From a7595eddaf84954a78f11c01ade8fbb1c14e3ef9 Mon Sep 17 00:00:00 2001 From: whywaita Date: Thu, 20 Aug 2026 14:23:46 +0900 Subject: [PATCH 2/5] ci: bump Dockerfile builder image to golang:1.27 go.mod now requires Go >= 1.27 (standard library uuid), and the Dockerfile uses GOTOOLCHAIN=local, so the builder image must be 1.27. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b4b9421..a70e1ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.25 AS builder +FROM golang:1.27 AS builder WORKDIR /go/src/github.com/whywaita/myshoes From 417ced13c2a4c890d965a232c4b4075c706a6ee4 Mon Sep 17 00:00:00 2001 From: whywaita Date: Thu, 20 Aug 2026 14:30:54 +0900 Subject: [PATCH 3/5] ci: pin staticcheck to v0.8.0-rc.1 for Go 1.27 support staticcheck@latest resolves to v0.7.0 (2026.1), whose bundled x/tools cannot decode Go 1.27's export data (version 4) and fails with an internal error. v0.8.0-rc.1 (2026.2rc1) supports Go 1.27 and passes cleanly on the module. --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b0b578f..422c52d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -25,7 +25,7 @@ jobs: go-version-file: 'go.mod' - name: lint run: | - go install honnef.co/go/tools/cmd/staticcheck@latest + go install honnef.co/go/tools/cmd/staticcheck@v0.8.0-rc.1 staticcheck ./... - name: vet run: | From 23e0caef1f98fb34aeaadfed7cbbe94ba8af0d0e Mon Sep 17 00:00:00 2001 From: whywaita Date: Thu, 20 Aug 2026 14:43:34 +0900 Subject: [PATCH 4/5] fix(datastore/mysql): bind and scan UUIDs as strings with std uuid The standard library uuid.UUID is a [16]byte with no driver.Valuer or sql.Scanner, unlike satori/go.uuid. UUID columns are VARCHAR(36), so: - Writes: pass uuid.UUID.String() when binding (CreateTarget, EnqueueJob). - Reads: scan rows into shadow structs whose UUID columns are strings, then parse back into datastore entities with uuid.Parse. Keeps the domain type as std uuid.UUID (no ripple into memory/web/runner/ starter) and confines DB conversion to the mysql package. --- pkg/datastore/mysql/job.go | 56 +++++++++++++++++++++-- pkg/datastore/mysql/runner.go | 83 ++++++++++++++++++++++++++++++----- pkg/datastore/mysql/target.go | 74 +++++++++++++++++++++++++++---- 3 files changed, 190 insertions(+), 23 deletions(-) diff --git a/pkg/datastore/mysql/job.go b/pkg/datastore/mysql/job.go index 92a778f..6b9a966 100644 --- a/pkg/datastore/mysql/job.go +++ b/pkg/datastore/mysql/job.go @@ -5,15 +5,63 @@ import ( "database/sql" "errors" "fmt" + "time" "github.com/whywaita/myshoes/pkg/datastore" "uuid" ) +// rowJob mirrors datastore.Job but stores the UUID columns (uuid, target_id) as +// strings so that database/sql can scan the VARCHAR(36) columns. The standard +// library uuid.UUID is a [16]byte with no sql.Scanner. +type rowJob struct { + UUID string `db:"uuid"` + GHEDomain sql.NullString `db:"ghe_domain"` + Repository string `db:"repository"` + CheckEventJSON string `db:"check_event"` + TargetID string `db:"target_id"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +func (r rowJob) job() (datastore.Job, error) { + u, err := uuid.Parse(r.UUID) + if err != nil { + return datastore.Job{}, fmt.Errorf("failed to parse job uuid %q: %w", r.UUID, err) + } + + tid, err := uuid.Parse(r.TargetID) + if err != nil { + return datastore.Job{}, fmt.Errorf("failed to parse target id %q: %w", r.TargetID, err) + } + + return datastore.Job{ + UUID: u, + GHEDomain: r.GHEDomain, + Repository: r.Repository, + CheckEventJSON: r.CheckEventJSON, + TargetID: tid, + CreatedAt: r.CreatedAt, + UpdatedAt: r.UpdatedAt, + }, nil +} + +func jobsFromRows(rows []rowJob) ([]datastore.Job, error) { + js := make([]datastore.Job, 0, len(rows)) + for _, r := range rows { + j, err := r.job() + if err != nil { + return nil, err + } + js = append(js, j) + } + return js, nil +} + // EnqueueJob add a job func (m *MySQL) EnqueueJob(ctx context.Context, job datastore.Job) error { query := `INSERT INTO jobs(uuid, ghe_domain, repository, check_event, target_id) VALUES (?, ?, ?, ?, ?)` - if _, err := m.Conn.ExecContext(ctx, query, job.UUID, job.GHEDomain, job.Repository, job.CheckEventJSON, job.TargetID.String()); err != nil { + if _, err := m.Conn.ExecContext(ctx, query, job.UUID.String(), job.GHEDomain, job.Repository, job.CheckEventJSON, job.TargetID.String()); err != nil { return fmt.Errorf("failed to execute INSERT query: %w", err) } @@ -29,9 +77,9 @@ func (m *MySQL) EnqueueJob(ctx context.Context, job datastore.Job) error { // ListJobs get all jobs func (m *MySQL) ListJobs(ctx context.Context) ([]datastore.Job, error) { - var jobs []datastore.Job + var rows []rowJob query := `SELECT uuid, ghe_domain, repository, check_event, target_id, created_at, updated_at FROM jobs` - if err := m.Conn.SelectContext(ctx, &jobs, query); err != nil { + if err := m.Conn.SelectContext(ctx, &rows, query); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, datastore.ErrNotFound } @@ -39,7 +87,7 @@ func (m *MySQL) ListJobs(ctx context.Context) ([]datastore.Job, error) { return nil, fmt.Errorf("failed to execute SELECT query: %w", err) } - return jobs, nil + return jobsFromRows(rows) } // DeleteJob delete a job diff --git a/pkg/datastore/mysql/runner.go b/pkg/datastore/mysql/runner.go index 5dc8c33..102fe97 100644 --- a/pkg/datastore/mysql/runner.go +++ b/pkg/datastore/mysql/runner.go @@ -11,6 +11,63 @@ import ( "uuid" ) +// rowRunner mirrors datastore.Runner but stores the UUID columns (runner_id, +// target_id) as strings so that database/sql can scan the VARCHAR(36) columns. +// The standard library uuid.UUID is a [16]byte with no sql.Scanner. +type rowRunner struct { + UUID string `db:"runner_id"` + ShoesType string `db:"shoes_type"` + IPAddress string `db:"ip_address"` + TargetID string `db:"target_id"` + CloudID string `db:"cloud_id"` + ResourceType datastore.ResourceType `db:"resource_type"` + RunnerUser sql.NullString `db:"runner_user"` + ProviderURL sql.NullString `db:"provider_url"` + RepositoryURL string `db:"repository_url"` + RequestWebhook string `db:"request_webhook"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +func (r rowRunner) runner() (datastore.Runner, error) { + u, err := uuid.Parse(r.UUID) + if err != nil { + return datastore.Runner{}, fmt.Errorf("failed to parse runner uuid %q: %w", r.UUID, err) + } + + tid, err := uuid.Parse(r.TargetID) + if err != nil { + return datastore.Runner{}, fmt.Errorf("failed to parse target id %q: %w", r.TargetID, err) + } + + return datastore.Runner{ + UUID: u, + ShoesType: r.ShoesType, + IPAddress: r.IPAddress, + TargetID: tid, + CloudID: r.CloudID, + ResourceType: r.ResourceType, + RunnerUser: r.RunnerUser, + ProviderURL: r.ProviderURL, + RepositoryURL: r.RepositoryURL, + RequestWebhook: r.RequestWebhook, + CreatedAt: r.CreatedAt, + UpdatedAt: r.UpdatedAt, + }, nil +} + +func runnersFromRows(rows []rowRunner) ([]datastore.Runner, error) { + rs := make([]datastore.Runner, 0, len(rows)) + for _, r := range rows { + runner, err := r.runner() + if err != nil { + return nil, err + } + rs = append(rs, runner) + } + return rs, nil +} + // CreateRunner add a runner func (m *MySQL) CreateRunner(ctx context.Context, runner datastore.Runner) error { tx := m.Conn.MustBegin() @@ -42,10 +99,10 @@ func (m *MySQL) CreateRunner(ctx context.Context, runner datastore.Runner) error // ListRunners get a not deleted runners func (m *MySQL) ListRunners(ctx context.Context) ([]datastore.Runner, error) { - var runners []datastore.Runner + var rows []rowRunner query := `SELECT runner.runner_id, detail.shoes_type, detail.ip_address, detail.target_id, detail.cloud_id, detail.created_at, detail.updated_at, detail.resource_type, detail.repository_url, detail.request_webhook, detail.runner_user, detail.provider_url FROM runners_running AS runner JOIN runner_detail AS detail ON runner.runner_id = detail.runner_id` - err := m.Conn.SelectContext(ctx, &runners, query) + err := m.Conn.SelectContext(ctx, &rows, query) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, datastore.ErrNotFound @@ -54,15 +111,15 @@ func (m *MySQL) ListRunners(ctx context.Context) ([]datastore.Runner, error) { return nil, fmt.Errorf("failed to execute SELECT query: %w", err) } - return runners, nil + return runnersFromRows(rows) } // ListRunnersByTargetID get a not deleted runners that has target_id func (m *MySQL) ListRunnersByTargetID(ctx context.Context, targetID uuid.UUID) ([]datastore.Runner, error) { - var runners []datastore.Runner + var rows []rowRunner query := `SELECT runner.runner_id, detail.shoes_type, detail.ip_address, detail.target_id, detail.cloud_id, detail.created_at, detail.updated_at, detail.resource_type, detail.repository_url, detail.request_webhook, detail.runner_user, detail.provider_url FROM runners_running AS runner JOIN runner_detail AS detail ON runner.runner_id = detail.runner_id WHERE detail.target_id = ?` - err := m.Conn.SelectContext(ctx, &runners, query, targetID) + err := m.Conn.SelectContext(ctx, &rows, query, targetID.String()) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, datastore.ErrNotFound @@ -71,15 +128,15 @@ func (m *MySQL) ListRunnersByTargetID(ctx context.Context, targetID uuid.UUID) ( return nil, fmt.Errorf("failed to execute SELECT query: %w", err) } - return runners, nil + return runnersFromRows(rows) } // ListRunnersLogBySince ListRunnerLog get a runners since time func (m *MySQL) ListRunnersLogBySince(ctx context.Context, since time.Time) ([]datastore.Runner, error) { - var runners []datastore.Runner + var rows []rowRunner query := `SELECT runner_id, shoes_type, ip_address, target_id, cloud_id, created_at, updated_at, resource_type, repository_url, request_webhook, runner_user, provider_url FROM runner_detail WHERE created_at > ?` - err := m.Conn.SelectContext(ctx, &runners, query, since) + err := m.Conn.SelectContext(ctx, &rows, query, since) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, datastore.ErrNotFound @@ -88,15 +145,15 @@ func (m *MySQL) ListRunnersLogBySince(ctx context.Context, since time.Time) ([]d return nil, fmt.Errorf("failed to execute SELECT query: %w", err) } - return runners, nil + return runnersFromRows(rows) } // GetRunner get a runner func (m *MySQL) GetRunner(ctx context.Context, id uuid.UUID) (*datastore.Runner, error) { - var r datastore.Runner + var row rowRunner query := `SELECT runner_id, shoes_type, ip_address, target_id, cloud_id, created_at, updated_at, resource_type, repository_url, request_webhook, runner_user, provider_url FROM runner_detail WHERE runner_id = ?` - if err := m.Conn.GetContext(ctx, &r, query, id.String()); err != nil { + if err := m.Conn.GetContext(ctx, &row, query, id.String()); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, datastore.ErrNotFound } @@ -104,6 +161,10 @@ func (m *MySQL) GetRunner(ctx context.Context, id uuid.UUID) (*datastore.Runner, return nil, fmt.Errorf("failed to execute SELECT query: %w", err) } + r, err := row.runner() + if err != nil { + return nil, err + } return &r, nil } diff --git a/pkg/datastore/mysql/target.go b/pkg/datastore/mysql/target.go index 0195b7e..a9c1da9 100644 --- a/pkg/datastore/mysql/target.go +++ b/pkg/datastore/mysql/target.go @@ -11,6 +11,56 @@ import ( "uuid" ) +// rowTarget mirrors datastore.Target but stores the UUID column as a string so +// that database/sql can scan the VARCHAR(36) uuid column. The standard library +// uuid.UUID is a [16]byte with no sql.Scanner, so it cannot be scanned directly. +type rowTarget struct { + UUID string `db:"uuid"` + Scope string `db:"scope"` + GitHubToken string `db:"github_token"` + TokenExpiredAt time.Time `db:"token_expired_at"` + GHEDomain sql.NullString `db:"ghe_domain"` + ResourceType datastore.ResourceType `db:"resource_type"` + ProviderURL sql.NullString `db:"provider_url"` + Status datastore.TargetStatus `db:"status"` + StatusDescription sql.NullString `db:"status_description"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +func (r rowTarget) target() (datastore.Target, error) { + u, err := uuid.Parse(r.UUID) + if err != nil { + return datastore.Target{}, fmt.Errorf("failed to parse target uuid %q: %w", r.UUID, err) + } + + return datastore.Target{ + UUID: u, + Scope: r.Scope, + GitHubToken: r.GitHubToken, + TokenExpiredAt: r.TokenExpiredAt, + GHEDomain: r.GHEDomain, + ResourceType: r.ResourceType, + ProviderURL: r.ProviderURL, + Status: r.Status, + StatusDescription: r.StatusDescription, + CreatedAt: r.CreatedAt, + UpdatedAt: r.UpdatedAt, + }, nil +} + +func targetsFromRows(rows []rowTarget) ([]datastore.Target, error) { + ts := make([]datastore.Target, 0, len(rows)) + for _, r := range rows { + t, err := r.target() + if err != nil { + return nil, err + } + ts = append(ts, t) + } + return ts, nil +} + // CreateTarget create a target func (m *MySQL) CreateTarget(ctx context.Context, target datastore.Target) error { expiredAtRFC3339 := target.TokenExpiredAt.Format("2006-01-02 15:04:05") @@ -19,7 +69,7 @@ func (m *MySQL) CreateTarget(ctx context.Context, target datastore.Target) error if _, err := m.Conn.ExecContext( ctx, query, - target.UUID, + target.UUID.String(), target.Scope, target.GHEDomain, target.GitHubToken, @@ -35,9 +85,9 @@ func (m *MySQL) CreateTarget(ctx context.Context, target datastore.Target) error // GetTarget get a target func (m *MySQL) GetTarget(ctx context.Context, id uuid.UUID) (*datastore.Target, error) { - var t datastore.Target + var row rowTarget query := `SELECT uuid, scope, github_token, token_expired_at, resource_type, provider_url, status, status_description, created_at, updated_at FROM targets WHERE uuid = ?` - if err := m.Conn.GetContext(ctx, &t, query, id.String()); err != nil { + if err := m.Conn.GetContext(ctx, &row, query, id.String()); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, datastore.ErrNotFound } @@ -45,14 +95,18 @@ func (m *MySQL) GetTarget(ctx context.Context, id uuid.UUID) (*datastore.Target, return nil, fmt.Errorf("failed to execute SELECT query: %w", err) } + t, err := row.target() + if err != nil { + return nil, err + } return &t, nil } // GetTargetByScope get a target from scope func (m *MySQL) GetTargetByScope(ctx context.Context, scope string) (*datastore.Target, error) { - var t datastore.Target + var row rowTarget query := `SELECT uuid, scope, github_token, token_expired_at, resource_type, provider_url, status, status_description, created_at, updated_at FROM targets WHERE scope = ?` - if err := m.Conn.GetContext(ctx, &t, query, scope); err != nil { + if err := m.Conn.GetContext(ctx, &row, query, scope); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, datastore.ErrNotFound } @@ -60,18 +114,22 @@ func (m *MySQL) GetTargetByScope(ctx context.Context, scope string) (*datastore. return nil, fmt.Errorf("failed to execute SELECT query: %w", err) } + t, err := row.target() + if err != nil { + return nil, err + } return &t, nil } // ListTargets get a all target func (m *MySQL) ListTargets(ctx context.Context) ([]datastore.Target, error) { - var ts []datastore.Target + var rows []rowTarget query := `SELECT uuid, scope, github_token, token_expired_at, resource_type, provider_url, status, status_description, created_at, updated_at FROM targets` - if err := m.Conn.SelectContext(ctx, &ts, query); err != nil { + if err := m.Conn.SelectContext(ctx, &rows, query); err != nil { return nil, fmt.Errorf("failed to SELECT query: %w", err) } - return ts, nil + return targetsFromRows(rows) } // DeleteTarget delete a target From b95ca613e8709d9a4025cd99ae4e59bd5d1b9d77 Mon Sep 17 00:00:00 2001 From: whywaita Date: Thu, 20 Aug 2026 14:49:57 +0900 Subject: [PATCH 5/5] fix(datastore/mysql_test): bind UUIDs as strings in test SQL helpers The sql.*FromSQL test helpers bound uuid.UUID directly to prepared statements, which fails with std uuid (no driver.Valuer/Scanner). Bind uuid.String() and scan into local shadow rows (UUID columns as strings) then parse back into datastore entities. --- pkg/datastore/mysql/job_test.go | 36 +++++++++++++++++-- pkg/datastore/mysql/runner_test.go | 58 +++++++++++++++++++++++++----- pkg/datastore/mysql/target_test.go | 40 +++++++++++++++++++-- 3 files changed, 119 insertions(+), 15 deletions(-) diff --git a/pkg/datastore/mysql/job_test.go b/pkg/datastore/mysql/job_test.go index aef7492..b2e666e 100644 --- a/pkg/datastore/mysql/job_test.go +++ b/pkg/datastore/mysql/job_test.go @@ -206,16 +206,46 @@ func TestMySQL_DeleteJob(t *testing.T) { } } +type sqlJobRow struct { + UUID string `db:"uuid"` + GHEDomain sql.NullString `db:"ghe_domain"` + Repository string `db:"repository"` + CheckEventJSON string `db:"check_event"` + TargetID string `db:"target_id"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +func (r sqlJobRow) job() (*datastore.Job, error) { + u, err := uuid.Parse(r.UUID) + if err != nil { + return nil, err + } + tid, err := uuid.Parse(r.TargetID) + if err != nil { + return nil, err + } + return &datastore.Job{ + UUID: u, + GHEDomain: r.GHEDomain, + Repository: r.Repository, + CheckEventJSON: r.CheckEventJSON, + TargetID: tid, + CreatedAt: r.CreatedAt, + UpdatedAt: r.UpdatedAt, + }, nil +} + func getJobFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Job, error) { - var j datastore.Job + var row sqlJobRow query := `SELECT uuid, ghe_domain, repository, check_event, target_id FROM jobs WHERE uuid = ?` stmt, err := testDB.Preparex(query) if err != nil { return nil, fmt.Errorf("failed to prepare: %w", err) } - err = stmt.Get(&j, id) + err = stmt.Get(&row, id.String()) if err != nil { return nil, fmt.Errorf("failed to get job: %w", err) } - return &j, nil + return row.job() } diff --git a/pkg/datastore/mysql/runner_test.go b/pkg/datastore/mysql/runner_test.go index 3816961..610291e 100644 --- a/pkg/datastore/mysql/runner_test.go +++ b/pkg/datastore/mysql/runner_test.go @@ -462,46 +462,86 @@ func TestMySQL_DeleteRunner(t *testing.T) { } } +type sqlRunnerRow struct { + UUID string `db:"runner_id"` + ShoesType string `db:"shoes_type"` + IPAddress string `db:"ip_address"` + TargetID string `db:"target_id"` + CloudID string `db:"cloud_id"` + ResourceType datastore.ResourceType `db:"resource_type"` + RunnerUser sql.NullString `db:"runner_user"` + ProviderURL sql.NullString `db:"provider_url"` + RepositoryURL string `db:"repository_url"` + RequestWebhook string `db:"request_webhook"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +func (r sqlRunnerRow) runner() (*datastore.Runner, error) { + u, err := uuid.Parse(r.UUID) + if err != nil { + return nil, err + } + tid, err := uuid.Parse(r.TargetID) + if err != nil { + return nil, err + } + return &datastore.Runner{ + UUID: u, + ShoesType: r.ShoesType, + IPAddress: r.IPAddress, + TargetID: tid, + CloudID: r.CloudID, + ResourceType: r.ResourceType, + RunnerUser: r.RunnerUser, + ProviderURL: r.ProviderURL, + RepositoryURL: r.RepositoryURL, + RequestWebhook: r.RequestWebhook, + CreatedAt: r.CreatedAt, + UpdatedAt: r.UpdatedAt, + }, nil +} + func getRunnerFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Runner, error) { - var r datastore.Runner + var row sqlRunnerRow query := `SELECT runner_id, shoes_type, ip_address, target_id, cloud_id, created_at, updated_at, resource_type, repository_url, request_webhook, runner_user, provider_url FROM runner_detail WHERE runner_id = ?` stmt, err := testDB.Preparex(query) if err != nil { return nil, fmt.Errorf("failed to prepare: %w", err) } - err = stmt.Get(&r, id) + err = stmt.Get(&row, id.String()) if err != nil { return nil, fmt.Errorf("failed to get runner: %w", err) } - return &r, nil + return row.runner() } func getRunningRunnerFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Runner, error) { - var r datastore.Runner + var row sqlRunnerRow query := `SELECT detail.runner_id, shoes_type, ip_address, target_id, cloud_id, detail.created_at, updated_at, detail.resource_type, detail.repository_url, detail.request_webhook FROM runner_detail AS detail JOIN runnesr_running AS running ON detail.runner_id = running.runner_id WHERE detail.runner_id = ?` stmt, err := testDB.Preparex(query) if err != nil { return nil, fmt.Errorf("failed to prepare: %w", err) } - err = stmt.Get(&r, id) + err = stmt.Get(&row, id.String()) if err != nil { return nil, fmt.Errorf("failed to get runner: %w", err) } - return &r, nil + return row.runner() } func getDeletedRunnerFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Runner, error) { - var r datastore.Runner + var row sqlRunnerRow query := `SELECT detail.runner_id, shoes_type, ip_address, target_id, cloud_id, detail.created_at, updated_at, detail.resource_type, detail.repository_url, detail.request_webhook FROM runner_detail AS detail JOIN runners_deleted AS deleted ON detail.runner_id = deleted.runner_id WHERE detail.runner_id = ?` stmt, err := testDB.Preparex(query) if err != nil { return nil, fmt.Errorf("failed to prepare: %w", err) } - err = stmt.Get(&r, id) + err = stmt.Get(&row, id.String()) if err != nil { return nil, fmt.Errorf("failed to get runner: %w", err) } - return &r, nil + return row.runner() } diff --git a/pkg/datastore/mysql/target_test.go b/pkg/datastore/mysql/target_test.go index 75e3f44..c7e4aad 100644 --- a/pkg/datastore/mysql/target_test.go +++ b/pkg/datastore/mysql/target_test.go @@ -865,16 +865,50 @@ func TestMySQL_UpdateTargetParam(t *testing.T) { } } +type sqlTargetRow struct { + UUID string `db:"uuid"` + Scope string `db:"scope"` + GitHubToken string `db:"github_token"` + TokenExpiredAt time.Time `db:"token_expired_at"` + GHEDomain sql.NullString `db:"ghe_domain"` + ResourceType datastore.ResourceType `db:"resource_type"` + ProviderURL sql.NullString `db:"provider_url"` + Status datastore.TargetStatus `db:"status"` + StatusDescription sql.NullString `db:"status_description"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +func (r sqlTargetRow) target() (*datastore.Target, error) { + u, err := uuid.Parse(r.UUID) + if err != nil { + return nil, err + } + return &datastore.Target{ + UUID: u, + Scope: r.Scope, + GitHubToken: r.GitHubToken, + TokenExpiredAt: r.TokenExpiredAt, + GHEDomain: r.GHEDomain, + ResourceType: r.ResourceType, + ProviderURL: r.ProviderURL, + Status: r.Status, + StatusDescription: r.StatusDescription, + CreatedAt: r.CreatedAt, + UpdatedAt: r.UpdatedAt, + }, nil +} + func getTargetFromSQL(testDB *sqlx.DB, uuid uuid.UUID) (*datastore.Target, error) { - var t datastore.Target + var row sqlTargetRow query := `SELECT uuid, scope, ghe_domain, github_token, token_expired_at, resource_type, provider_url, status, status_description, created_at, updated_at FROM targets WHERE uuid = ?` stmt, err := testDB.Preparex(query) if err != nil { return nil, fmt.Errorf("failed to prepare: %w", err) } - err = stmt.Get(&t, uuid) + err = stmt.Get(&row, uuid.String()) if err != nil { return nil, fmt.Errorf("failed to get target: %w", err) } - return &t, nil + return row.target() }