• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

golang-migrate / migrate / 24640136570

19 Apr 2026 09:58PM UTC coverage: 56.328% (+1.9%) from 54.432%
24640136570

Pull #1394

github

joschi
fix: address second round of Copilot review comments

- README.md: fix db.system → db.system.name in database span table
- otel.go: call otel.Handle(err) on instrument creation errors instead
  of silently discarding them (doc comment already promised this)
- migrate.go: use attribute.Int64 for uint version fields to avoid
  int overflow on 32-bit platforms
- migrate.go: sanitize database.Error in otelSpanSetError to avoid
  leaking migration SQL into trace span status descriptions
- database/oteldriver.go: same SQL-leak fix in endSpan; add errors import
- source/oteldriver.go: use attribute.Int64 for uint version fields
- source/oteldriver_test.go: assert span.Status().Code != codes.Error
  directly rather than comparing full sdktrace.Status struct
- source/pkger/pkger.go: fix import grouping (stdlib before third-party)
- source/google_cloud_storage/storage.go: merge context into stdlib group

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pull Request #1394: feat: OpenTelemetry instrumentation

1020 of 1236 new or added lines in 47 files covered. (82.52%)

8 existing lines in 7 files now uncovered.

4731 of 8399 relevant lines covered (56.33%)

50.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

44.9
/source/github/github.go
1
package github
2

3
import (
4
        "context"
5
        "fmt"
6
        "io"
7
        "net/http"
8
        nurl "net/url"
9
        "os"
10
        "path"
11
        "strings"
12

13
        "golang.org/x/oauth2"
14

15
        "github.com/golang-migrate/migrate/v4/source"
16
        "github.com/google/go-github/v39/github"
17
        "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
18
)
19

20
func init() {
2✔
21
        source.Register("github", &Github{})
2✔
22
}
2✔
23

24
var (
25
        ErrNoUserInfo          = fmt.Errorf("no username:token provided")
26
        ErrNoAccessToken       = fmt.Errorf("no access token")
27
        ErrInvalidRepo         = fmt.Errorf("invalid repo")
28
        ErrInvalidGithubClient = fmt.Errorf("expected *github.Client")
29
        ErrNoDir               = fmt.Errorf("no directory")
30
)
31

32
type Github struct {
33
        config     *Config
34
        client     *github.Client
35
        options    *github.RepositoryContentGetOptions
36
        migrations *source.Migrations
37
}
38

39
type Config struct {
40
        Owner string
41
        Repo  string
42
        Path  string
43
        Ref   string
44
}
45

46
func (g *Github) Open(ctx context.Context, url string) (source.Driver, error) {
2✔
47
        u, err := nurl.Parse(url)
2✔
48
        if err != nil {
2✔
49
                return nil, err
×
50
        }
×
51

52
        // client defaults to an otelhttp-instrumented client
53
        base := &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
2✔
54
        var client *http.Client
2✔
55
        if u.User != nil {
2✔
56
                password, ok := u.User.Password()
×
57
                if !ok {
×
58
                        return nil, ErrNoUserInfo
×
59
                }
×
60
                ts := oauth2.StaticTokenSource(
×
61
                        &oauth2.Token{AccessToken: password},
×
62
                )
×
NEW
63
                ctx = context.WithValue(ctx, oauth2.HTTPClient, base)
×
NEW
64
                client = oauth2.NewClient(ctx, ts)
×
65

66
        } else {
2✔
67
                client = base
2✔
68
        }
2✔
69

70
        gn := &Github{
2✔
71
                client:     github.NewClient(client),
2✔
72
                migrations: source.NewMigrations(),
2✔
73
                options:    &github.RepositoryContentGetOptions{Ref: u.Fragment},
2✔
74
        }
2✔
75

2✔
76
        gn.ensureFields()
2✔
77

2✔
78
        // set owner, repo and path in repo
2✔
79
        gn.config.Owner = u.Host
2✔
80
        pe := strings.Split(strings.Trim(u.Path, "/"), "/")
2✔
81
        if len(pe) < 1 {
2✔
82
                return nil, ErrInvalidRepo
×
83
        }
×
84
        gn.config.Repo = pe[0]
2✔
85
        if len(pe) > 1 {
4✔
86
                gn.config.Path = strings.Join(pe[1:], "/")
2✔
87
        }
2✔
88

89
        if err := gn.readDirectory(ctx); err != nil {
2✔
90
                return nil, err
×
91
        }
×
92

93
        return gn, nil
2✔
94
}
95

NEW
96
func WithInstance(ctx context.Context, client *github.Client, config *Config) (source.Driver, error) {
×
97
        gn := &Github{
×
98
                client:     client,
×
99
                config:     config,
×
100
                migrations: source.NewMigrations(),
×
101
                options:    &github.RepositoryContentGetOptions{Ref: config.Ref},
×
102
        }
×
103

×
NEW
104
        if err := gn.readDirectory(ctx); err != nil {
×
105
                return nil, err
×
106
        }
×
107

108
        return gn, nil
×
109
}
110

111
func (g *Github) readDirectory(ctx context.Context) error {
2✔
112
        g.ensureFields()
2✔
113

2✔
114
        fileContent, dirContents, _, err := g.client.Repositories.GetContents(
2✔
115
                ctx,
2✔
116
                g.config.Owner,
2✔
117
                g.config.Repo,
2✔
118
                g.config.Path,
2✔
119
                g.options,
2✔
120
        )
2✔
121

2✔
122
        if err != nil {
2✔
123
                return err
×
124
        }
×
125
        if fileContent != nil {
2✔
126
                return ErrNoDir
×
127
        }
×
128

129
        for _, fi := range dirContents {
30✔
130
                m, err := source.DefaultParse(*fi.Name)
28✔
131
                if err != nil {
28✔
132
                        continue // ignore files that we can't parse
×
133
                }
134
                if !g.migrations.Append(m) {
28✔
135
                        return fmt.Errorf("unable to parse file %v", *fi.Name)
×
136
                }
×
137
        }
138

139
        return nil
2✔
140
}
141

142
func (g *Github) ensureFields() {
8✔
143
        if g.config == nil {
10✔
144
                g.config = &Config{}
2✔
145
        }
2✔
146
}
147

NEW
148
func (g *Github) Close(ctx context.Context) error {
×
149
        return nil
×
150
}
×
151

152
func (g *Github) First(ctx context.Context) (version uint, err error) {
2✔
153
        g.ensureFields()
2✔
154

2✔
155
        if v, ok := g.migrations.First(ctx); !ok {
2✔
156
                return 0, &os.PathError{Op: "first", Path: g.config.Path, Err: os.ErrNotExist}
×
157
        } else {
2✔
158
                return v, nil
2✔
159
        }
2✔
160
}
161

NEW
162
func (g *Github) Prev(ctx context.Context, version uint) (prevVersion uint, err error) {
×
163
        g.ensureFields()
×
164

×
NEW
165
        if v, ok := g.migrations.Prev(ctx, version); !ok {
×
166
                return 0, &os.PathError{Op: fmt.Sprintf("prev for version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
×
167
        } else {
×
168
                return v, nil
×
169
        }
×
170
}
171

172
func (g *Github) Next(ctx context.Context, version uint) (nextVersion uint, err error) {
2✔
173
        g.ensureFields()
2✔
174

2✔
175
        if v, ok := g.migrations.Next(ctx, version); !ok {
2✔
176
                return 0, &os.PathError{Op: fmt.Sprintf("next for version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
×
177
        } else {
2✔
178
                return v, nil
2✔
179
        }
2✔
180
}
181

NEW
182
func (g *Github) ReadUp(ctx context.Context, version uint) (r io.ReadCloser, identifier string, err error) {
×
183
        g.ensureFields()
×
184

×
NEW
185
        if m, ok := g.migrations.Up(ctx, version); ok {
×
186
                r, _, err := g.client.Repositories.DownloadContents(
×
NEW
187
                        ctx,
×
188
                        g.config.Owner,
×
189
                        g.config.Repo,
×
190
                        path.Join(g.config.Path, m.Raw),
×
191
                        g.options,
×
192
                )
×
193

×
194
                if err != nil {
×
195
                        return nil, "", err
×
196
                }
×
197
                return r, m.Identifier, nil
×
198
        }
199
        return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
×
200
}
201

NEW
202
func (g *Github) ReadDown(ctx context.Context, version uint) (r io.ReadCloser, identifier string, err error) {
×
203
        g.ensureFields()
×
204

×
NEW
205
        if m, ok := g.migrations.Down(ctx, version); ok {
×
206
                r, _, err := g.client.Repositories.DownloadContents(
×
NEW
207
                        ctx,
×
208
                        g.config.Owner,
×
209
                        g.config.Repo,
×
210
                        path.Join(g.config.Path, m.Raw),
×
211
                        g.options,
×
212
                )
×
213

×
214
                if err != nil {
×
215
                        return nil, "", err
×
216
                }
×
217
                return r, m.Identifier, nil
×
218
        }
219
        return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
×
220
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc