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

supabase / cli / 19288913427

12 Nov 2025 06:47AM UTC coverage: 54.407% (-0.3%) from 54.689%
19288913427

Pull #4429

github

web-flow
Merge 1cc083715 into 408904212
Pull Request #4429: fix: mark database linking error as non-fatal

33 of 92 new or added lines in 6 files covered. (35.87%)

29 existing lines in 6 files now uncovered.

6358 of 11686 relevant lines covered (54.41%)

6.1 hits per line

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

28.85
/internal/bootstrap/bootstrap.go
1
package bootstrap
2

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

13
        "github.com/cenkalti/backoff/v4"
14
        "github.com/go-errors/errors"
15
        "github.com/google/go-github/v62/github"
16
        "github.com/jackc/pgconn"
17
        "github.com/jackc/pgx/v4"
18
        "github.com/joho/godotenv"
19
        "github.com/spf13/afero"
20
        "github.com/spf13/viper"
21
        "github.com/supabase/cli/internal/db/push"
22
        initBlank "github.com/supabase/cli/internal/init"
23
        "github.com/supabase/cli/internal/link"
24
        "github.com/supabase/cli/internal/login"
25
        "github.com/supabase/cli/internal/projects/apiKeys"
26
        "github.com/supabase/cli/internal/projects/create"
27
        "github.com/supabase/cli/internal/utils"
28
        "github.com/supabase/cli/internal/utils/flags"
29
        "github.com/supabase/cli/internal/utils/tenant"
30
        "github.com/supabase/cli/pkg/api"
31
        "github.com/supabase/cli/pkg/fetcher"
32
        "github.com/supabase/cli/pkg/queue"
33
        "golang.org/x/term"
34
)
35

36
func Run(ctx context.Context, starter StarterTemplate, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
×
37
        workdir := viper.GetString("WORKDIR")
×
38
        if !filepath.IsAbs(workdir) {
×
39
                workdir = filepath.Join(utils.CurrentDirAbs, workdir)
×
40
        }
×
41
        if err := utils.MkdirIfNotExistFS(fsys, workdir); err != nil {
×
42
                return err
×
43
        }
×
44
        if empty, err := afero.IsEmpty(fsys, workdir); err != nil {
×
45
                return errors.Errorf("failed to read workdir: %w", err)
×
46
        } else if !empty {
×
47
                title := fmt.Sprintf("Do you want to overwrite existing files in %s directory?", utils.Bold(workdir))
×
48
                if shouldOverwrite, err := utils.NewConsole().PromptYesNo(ctx, title, true); err != nil {
×
49
                        return err
×
50
                } else if !shouldOverwrite {
×
51
                        return errors.New(context.Canceled)
×
52
                }
×
53
        }
54
        if err := utils.ChangeWorkDir(fsys); err != nil {
×
55
                return err
×
56
        }
×
57
        // 0. Download starter template
58
        if len(starter.Url) > 0 {
×
59
                client := utils.GetGitHubClient(ctx)
×
60
                if err := downloadSample(ctx, client, starter.Url, fsys); err != nil {
×
61
                        return err
×
62
                }
×
63
        } else if err := initBlank.Run(ctx, fsys, nil, nil, utils.InitParams{Overwrite: true}); err != nil {
×
64
                return err
×
65
        }
×
66
        // 1. Login
67
        _, err := utils.LoadAccessTokenFS(fsys)
×
68
        if errors.Is(err, utils.ErrMissingToken) {
×
69
                if err := login.Run(ctx, os.Stdout, login.RunParams{
×
70
                        OpenBrowser: term.IsTerminal(int(os.Stdin.Fd())),
×
71
                        Fsys:        fsys,
×
72
                }); err != nil {
×
73
                        return err
×
74
                }
×
75
        } else if err != nil {
×
76
                return err
×
77
        }
×
78
        // 2. Create project
79
        params := api.V1CreateProjectBody{
×
80
                Name: filepath.Base(workdir),
×
81
        }
×
82
        if len(starter.Url) > 0 {
×
83
                params.TemplateUrl = &starter.Url
×
84
        }
×
85
        if err := create.Run(ctx, params, fsys); err != nil {
×
86
                return err
×
87
        }
×
88
        // 3. Get api keys
89
        var keys []api.ApiKeyResponse
×
90
        policy := utils.NewBackoffPolicy(ctx)
×
91
        if err := backoff.RetryNotify(func() error {
×
92
                fmt.Fprintln(os.Stderr, "Linking project...")
×
93
                keys, err = apiKeys.RunGetApiKeys(ctx, flags.ProjectRef)
×
94
                return err
×
95
        }, policy, utils.NewErrorCallback()); err != nil {
×
96
                return err
×
97
        }
×
98
        // 4. Link project
99
        if err := flags.LoadConfig(fsys); err != nil {
×
100
                return err
×
101
        }
×
102
        link.LinkServices(ctx, flags.ProjectRef, tenant.NewApiKey(keys).Anon, false, fsys)
×
103
        if err := utils.WriteFile(utils.ProjectRefPath, []byte(flags.ProjectRef), fsys); err != nil {
×
104
                return err
×
105
        }
×
106
        // 5. Wait for project healthy
107
        policy.Reset()
×
108
        if err := backoff.RetryNotify(func() error {
×
109
                fmt.Fprintln(os.Stderr, "Checking project health...")
×
110
                return checkProjectHealth(ctx)
×
111
        }, policy, utils.NewErrorCallback()); err != nil {
×
112
                return err
×
113
        }
×
114
        // 6. Push migrations
NEW
115
        config, err := flags.NewDbConfigWithPassword(ctx, flags.ProjectRef)
×
NEW
116
        if err != nil {
×
NEW
117
                fmt.Fprintln(os.Stderr, err)
×
NEW
118
        }
×
119
        if err := writeDotEnv(keys, config, fsys); err != nil {
×
120
                fmt.Fprintln(os.Stderr, "Failed to create .env file:", err)
×
121
        }
×
122
        policy.Reset()
×
123
        if err := backoff.RetryNotify(func() error {
×
124
                return push.Run(ctx, false, false, true, true, config, fsys)
×
125
        }, policy, utils.NewErrorCallback()); err != nil {
×
126
                return err
×
127
        }
×
128
        // 7. TODO: deploy functions
129
        utils.CmdSuggestion = suggestAppStart(utils.CurrentDirAbs, starter.Start)
×
130
        return nil
×
131
}
132

133
func suggestAppStart(cwd, command string) string {
3✔
134
        logger := utils.GetDebugLogger()
3✔
135
        workdir, err := os.Getwd()
3✔
136
        if err != nil {
3✔
137
                fmt.Fprintln(logger, err)
×
138
        }
×
139
        workdir, err = filepath.Rel(cwd, workdir)
3✔
140
        if err != nil {
4✔
141
                fmt.Fprintln(logger, err)
1✔
142
        }
1✔
143
        var cmd []string
3✔
144
        if len(workdir) > 0 && workdir != "." {
4✔
145
                cmd = append(cmd, "cd "+workdir)
1✔
146
        }
1✔
147
        if len(command) > 0 {
6✔
148
                cmd = append(cmd, command)
3✔
149
        }
3✔
150
        suggestion := "To start your app:"
3✔
151
        for _, c := range cmd {
7✔
152
                suggestion += fmt.Sprintf("\n  %s", utils.Aqua(c))
4✔
153
        }
4✔
154
        return suggestion
3✔
155
}
156

157
func checkProjectHealth(ctx context.Context) error {
×
158
        params := api.V1GetServicesHealthParams{
×
159
                Services: []api.V1GetServicesHealthParamsServices{api.Db},
×
160
        }
×
161
        resp, err := utils.GetSupabase().V1GetServicesHealthWithResponse(ctx, flags.ProjectRef, &params)
×
162
        if err != nil {
×
163
                return err
×
164
        }
×
165
        if resp.JSON200 == nil {
×
166
                return errors.Errorf("Error status %d: %s", resp.StatusCode(), resp.Body)
×
167
        }
×
168
        for _, service := range *resp.JSON200 {
×
169
                if !service.Healthy {
×
170
                        return errors.Errorf("Service not healthy: %s (%s)", service.Name, service.Status)
×
171
                }
×
172
        }
173
        return nil
×
174
}
175

176
const (
177
        SUPABASE_SERVICE_ROLE_KEY = "SUPABASE_SERVICE_ROLE_KEY"
178
        SUPABASE_ANON_KEY         = "SUPABASE_ANON_KEY"
179
        SUPABASE_URL              = "SUPABASE_URL"
180
        POSTGRES_URL              = "POSTGRES_URL"
181
        // Derived keys
182
        POSTGRES_PRISMA_URL           = "POSTGRES_PRISMA_URL"
183
        POSTGRES_URL_NON_POOLING      = "POSTGRES_URL_NON_POOLING"
184
        POSTGRES_USER                 = "POSTGRES_USER"
185
        POSTGRES_HOST                 = "POSTGRES_HOST"
186
        POSTGRES_PASSWORD             = "POSTGRES_PASSWORD" //nolint:gosec
187
        POSTGRES_DATABASE             = "POSTGRES_DATABASE"
188
        NEXT_PUBLIC_SUPABASE_ANON_KEY = "NEXT_PUBLIC_SUPABASE_ANON_KEY"
189
        NEXT_PUBLIC_SUPABASE_URL      = "NEXT_PUBLIC_SUPABASE_URL"
190
        EXPO_PUBLIC_SUPABASE_ANON_KEY = "EXPO_PUBLIC_SUPABASE_ANON_KEY"
191
        EXPO_PUBLIC_SUPABASE_URL      = "EXPO_PUBLIC_SUPABASE_URL"
192
)
193

194
func writeDotEnv(keys []api.ApiKeyResponse, config pgconn.Config, fsys afero.Fs) error {
4✔
195
        // Initialise default envs
4✔
196
        initial := apiKeys.ToEnv(keys)
4✔
197
        initial[SUPABASE_URL] = "https://" + utils.GetSupabaseHost(flags.ProjectRef)
4✔
198
        transactionMode := *config.Copy()
4✔
199
        transactionMode.Port = 6543
4✔
200
        initial[POSTGRES_URL] = utils.ToPostgresURL(transactionMode)
4✔
201
        // Populate from .env.example if exists
4✔
202
        envs, err := parseExampleEnv(fsys)
4✔
203
        if err != nil {
5✔
204
                return err
1✔
205
        }
1✔
206
        for k, v := range envs {
16✔
207
                switch k {
13✔
208
                case SUPABASE_SERVICE_ROLE_KEY:
1✔
209
                case SUPABASE_ANON_KEY:
1✔
210
                case SUPABASE_URL:
1✔
211
                case POSTGRES_URL:
1✔
212
                // Derived keys
213
                case POSTGRES_PRISMA_URL:
1✔
214
                        initial[k] = initial[POSTGRES_URL]
1✔
215
                case POSTGRES_URL_NON_POOLING:
1✔
216
                        initial[k] = utils.ToPostgresURL(config)
1✔
217
                case POSTGRES_USER:
1✔
218
                        initial[k] = config.User
1✔
219
                case POSTGRES_HOST:
1✔
220
                        initial[k] = config.Host
1✔
221
                case POSTGRES_PASSWORD:
1✔
222
                        initial[k] = config.Password
1✔
223
                case POSTGRES_DATABASE:
1✔
224
                        initial[k] = config.Database
1✔
225
                case NEXT_PUBLIC_SUPABASE_ANON_KEY:
1✔
226
                        fallthrough
1✔
227
                case EXPO_PUBLIC_SUPABASE_ANON_KEY:
1✔
228
                        initial[k] = initial[SUPABASE_ANON_KEY]
1✔
229
                case NEXT_PUBLIC_SUPABASE_URL:
1✔
230
                        fallthrough
1✔
231
                case EXPO_PUBLIC_SUPABASE_URL:
1✔
232
                        initial[k] = initial[SUPABASE_URL]
1✔
233
                default:
1✔
234
                        initial[k] = v
1✔
235
                }
236
        }
237
        // Write to .env file
238
        out, err := godotenv.Marshal(initial)
3✔
239
        if err != nil {
3✔
240
                return errors.Errorf("failed to marshal env map: %w", err)
×
241
        }
×
242
        return utils.WriteFile(".env", []byte(out), fsys)
3✔
243
}
244

245
func parseExampleEnv(fsys afero.Fs) (map[string]string, error) {
4✔
246
        path := ".env.example"
4✔
247
        f, err := fsys.Open(path)
4✔
248
        if errors.Is(err, os.ErrNotExist) {
6✔
249
                return nil, nil
2✔
250
        } else if err != nil {
4✔
251
                return nil, errors.Errorf("failed to open %s: %w", path, err)
×
252
        }
×
253
        defer f.Close()
2✔
254
        envs, err := godotenv.Parse(f)
2✔
255
        if err != nil {
3✔
256
                return nil, errors.Errorf("failed to parse %s: %w", path, err)
1✔
257
        }
1✔
258
        return envs, nil
1✔
259
}
260

261
type samplesRepo struct {
262
        Samples []StarterTemplate `json:"samples"`
263
}
264

265
type StarterTemplate struct {
266
        Name        string `json:"name"`
267
        Description string `json:"description"`
268
        Url         string `json:"url"`
269
        Start       string `json:"start"`
270
}
271

272
func ListSamples(ctx context.Context, client *github.Client) ([]StarterTemplate, error) {
×
273
        owner := "supabase-community"
×
274
        repo := "supabase-samples"
×
275
        path := "samples.json"
×
276
        ref := "main"
×
277
        opts := github.RepositoryContentGetOptions{Ref: ref}
×
278
        file, _, _, err := client.Repositories.GetContents(ctx, owner, repo, path, &opts)
×
279
        if err != nil {
×
280
                return nil, errors.Errorf("failed to list samples: %w", err)
×
281
        }
×
282
        content, err := file.GetContent()
×
283
        if err != nil {
×
284
                return nil, errors.Errorf("failed to decode samples: %w", err)
×
285
        }
×
286
        var data samplesRepo
×
287
        if err := json.Unmarshal([]byte(content), &data); err != nil {
×
288
                return nil, errors.Errorf("failed to unmarshal samples: %w", err)
×
289
        }
×
290
        return data.Samples, nil
×
291
}
292

293
func downloadSample(ctx context.Context, client *github.Client, templateUrl string, fsys afero.Fs) error {
×
294
        fmt.Println("Downloading:", templateUrl)
×
295
        // https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-user-management
×
296
        parsed, err := url.Parse(templateUrl)
×
297
        if err != nil {
×
298
                return errors.Errorf("failed to parse template url: %w", err)
×
299
        }
×
300
        parts := strings.Split(parsed.Path, "/")
×
301
        owner := parts[1]
×
302
        repo := parts[2]
×
303
        ref := parts[4]
×
304
        root := strings.Join(parts[5:], "/")
×
305
        opts := github.RepositoryContentGetOptions{Ref: ref}
×
306
        queue := make([]string, 0)
×
307
        queue = append(queue, root)
×
308
        download := NewDownloader(5, fsys)
×
309
        for len(queue) > 0 {
×
310
                contentPath := queue[0]
×
311
                queue = queue[1:]
×
312
                _, directory, _, err := client.Repositories.GetContents(ctx, owner, repo, contentPath, &opts)
×
313
                if err != nil {
×
314
                        return errors.Errorf("failed to download template: %w", err)
×
315
                }
×
316
                for _, file := range directory {
×
317
                        switch file.GetType() {
×
318
                        case "file":
×
319
                                path := strings.TrimPrefix(file.GetPath(), root)
×
320
                                hostPath := filepath.Join(".", filepath.FromSlash(path))
×
321
                                if err := download.Start(ctx, hostPath, file.GetDownloadURL()); err != nil {
×
322
                                        return err
×
323
                                }
×
324
                        case "dir":
×
325
                                queue = append(queue, file.GetPath())
×
326
                        default:
×
327
                                fmt.Fprintf(os.Stderr, "Ignoring %s: %s\n", file.GetType(), file.GetPath())
×
328
                        }
329
                }
330
        }
331
        return download.Wait()
×
332
}
333

334
type Downloader struct {
335
        api   *fetcher.Fetcher
336
        queue *queue.JobQueue
337
        fsys  afero.Fs
338
}
339

340
func NewDownloader(concurrency uint, fsys afero.Fs) *Downloader {
×
341
        return &Downloader{
×
342
                api:   fetcher.NewFetcher("", fetcher.WithExpectedStatus(http.StatusOK)),
×
343
                queue: queue.NewJobQueue(concurrency),
×
344
                fsys:  fsys,
×
345
        }
×
346
}
×
347

348
func (d *Downloader) Start(ctx context.Context, localPath, remotePath string) error {
×
349
        job := func() error {
×
350
                resp, err := d.api.Send(ctx, http.MethodGet, remotePath, nil)
×
351
                if err != nil {
×
352
                        return err
×
353
                }
×
354
                defer resp.Body.Close()
×
355
                if err := afero.WriteReader(d.fsys, localPath, resp.Body); err != nil {
×
356
                        return errors.Errorf("failed to write file: %w", err)
×
357
                }
×
358
                return nil
×
359
        }
360
        return d.queue.Put(job)
×
361
}
362

363
func (d *Downloader) Wait() error {
×
364
        return d.queue.Collect()
×
365
}
×
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

© 2025 Coveralls, Inc