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

supabase / cli / 8683400568

15 Apr 2024 04:28AM UTC coverage: 57.728% (-0.03%) from 57.755%
8683400568

Pull #2156

github

sweatybridge
fix: relative path from repo root directory
Pull Request #2156: fix: relative path from repo root directory

0 of 1 new or added line in 1 file covered. (0.0%)

5 existing lines in 1 file now uncovered.

6383 of 11057 relevant lines covered (57.73%)

662.06 hits per line

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

28.99
/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
        "sync"
13
        "time"
14

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

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

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

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

175
const maxRetries = 8
176

177
func newBackoffPolicy(ctx context.Context) backoff.BackOffContext {
×
178
        b := backoff.ExponentialBackOff{
×
179
                InitialInterval:     3 * time.Second,
×
180
                RandomizationFactor: backoff.DefaultRandomizationFactor,
×
181
                Multiplier:          backoff.DefaultMultiplier,
×
182
                MaxInterval:         backoff.DefaultMaxInterval,
×
183
                MaxElapsedTime:      backoff.DefaultMaxElapsedTime,
×
184
                Stop:                backoff.Stop,
×
185
                Clock:               backoff.SystemClock,
×
186
        }
×
187
        b.Reset()
×
188
        return backoff.WithContext(backoff.WithMaxRetries(&b, maxRetries), ctx)
×
189
}
×
190

191
func newErrorCallback() backoff.Notify {
×
192
        failureCount := 0
×
193
        logger := utils.GetDebugLogger()
×
194
        return func(err error, d time.Duration) {
×
195
                failureCount += 1
×
196
                fmt.Fprintln(logger, err)
×
197
                fmt.Fprintf(os.Stderr, "Retry (%d/%d): ", failureCount, maxRetries)
×
198
        }
×
199
}
200

201
const (
202
        SUPABASE_SERVICE_ROLE_KEY = "SUPABASE_SERVICE_ROLE_KEY"
203
        SUPABASE_ANON_KEY         = "SUPABASE_ANON_KEY"
204
        SUPABASE_URL              = "SUPABASE_URL"
205
        POSTGRES_URL              = "POSTGRES_URL"
206
        // Derived keys
207
        POSTGRES_PRISMA_URL           = "POSTGRES_PRISMA_URL"
208
        POSTGRES_URL_NON_POOLING      = "POSTGRES_URL_NON_POOLING"
209
        POSTGRES_USER                 = "POSTGRES_USER"
210
        POSTGRES_HOST                 = "POSTGRES_HOST"
211
        POSTGRES_PASSWORD             = "POSTGRES_PASSWORD" //nolint:gosec
212
        POSTGRES_DATABASE             = "POSTGRES_DATABASE"
213
        NEXT_PUBLIC_SUPABASE_ANON_KEY = "NEXT_PUBLIC_SUPABASE_ANON_KEY"
214
        NEXT_PUBLIC_SUPABASE_URL      = "NEXT_PUBLIC_SUPABASE_URL"
215
        EXPO_PUBLIC_SUPABASE_ANON_KEY = "EXPO_PUBLIC_SUPABASE_ANON_KEY"
216
        EXPO_PUBLIC_SUPABASE_URL      = "EXPO_PUBLIC_SUPABASE_URL"
217
)
218

219
func writeDotEnv(keys []api.ApiKeyResponse, config pgconn.Config, fsys afero.Fs) error {
4✔
220
        // Initialise default envs
4✔
221
        transactionMode := *config.Copy()
4✔
222
        transactionMode.Port = 6543
4✔
223
        initial := map[string]string{
4✔
224
                SUPABASE_URL: "https://" + utils.GetSupabaseHost(flags.ProjectRef),
4✔
225
                POSTGRES_URL: utils.ToPostgresURL(transactionMode),
4✔
226
        }
4✔
227
        for _, entry := range keys {
8✔
228
                name := strings.ToUpper(entry.Name)
4✔
229
                key := fmt.Sprintf("SUPABASE_%s_KEY", name)
4✔
230
                initial[key] = entry.ApiKey
4✔
231
        }
4✔
232
        // Populate from .env.example if exists
233
        envs, err := parseExampleEnv(fsys)
4✔
234
        if err != nil {
5✔
235
                return err
1✔
236
        }
1✔
237
        for k, v := range envs {
16✔
238
                switch k {
13✔
239
                case SUPABASE_SERVICE_ROLE_KEY:
1✔
240
                case SUPABASE_ANON_KEY:
1✔
241
                case SUPABASE_URL:
1✔
242
                case POSTGRES_URL:
1✔
243
                // Derived keys
244
                case POSTGRES_PRISMA_URL:
1✔
245
                        initial[k] = initial[POSTGRES_URL]
1✔
246
                case POSTGRES_URL_NON_POOLING:
1✔
247
                        initial[k] = utils.ToPostgresURL(config)
1✔
248
                case POSTGRES_USER:
1✔
249
                        initial[k] = config.User
1✔
250
                case POSTGRES_HOST:
1✔
251
                        initial[k] = config.Host
1✔
252
                case POSTGRES_PASSWORD:
1✔
253
                        initial[k] = config.Password
1✔
254
                case POSTGRES_DATABASE:
1✔
255
                        initial[k] = config.Database
1✔
256
                case NEXT_PUBLIC_SUPABASE_ANON_KEY:
1✔
257
                        fallthrough
1✔
258
                case EXPO_PUBLIC_SUPABASE_ANON_KEY:
1✔
259
                        initial[k] = initial[SUPABASE_ANON_KEY]
1✔
260
                case NEXT_PUBLIC_SUPABASE_URL:
1✔
261
                        fallthrough
1✔
262
                case EXPO_PUBLIC_SUPABASE_URL:
1✔
263
                        initial[k] = initial[SUPABASE_URL]
1✔
264
                default:
1✔
265
                        initial[k] = v
1✔
266
                }
267
        }
268
        // Write to .env file
269
        out, err := godotenv.Marshal(initial)
3✔
270
        if err != nil {
3✔
271
                return errors.Errorf("failed to marshal env map: %w", err)
×
272
        }
×
273
        return utils.WriteFile(".env", []byte(out), fsys)
3✔
274
}
275

276
func parseExampleEnv(fsys afero.Fs) (map[string]string, error) {
4✔
277
        path := ".env.example"
4✔
278
        f, err := fsys.Open(path)
4✔
279
        if errors.Is(err, os.ErrNotExist) {
6✔
280
                return nil, nil
2✔
281
        } else if err != nil {
4✔
282
                return nil, errors.Errorf("failed to open %s: %w", path, err)
×
283
        }
×
284
        defer f.Close()
2✔
285
        envs, err := godotenv.Parse(f)
2✔
286
        if err != nil {
3✔
287
                return nil, errors.Errorf("failed to parse %s: %w", path, err)
1✔
288
        }
1✔
289
        return envs, nil
1✔
290
}
291

292
var (
293
        githubClient *github.Client
294
        clientOnce   sync.Once
295
)
296

297
func GetGtihubClient(ctx context.Context) *github.Client {
×
298
        clientOnce.Do(func() {
×
299
                var client *http.Client
×
300
                token := os.Getenv("GITHUB_TOKEN")
×
301
                if len(token) > 0 {
×
302
                        ts := oauth2.StaticTokenSource(
×
303
                                &oauth2.Token{AccessToken: token},
×
304
                        )
×
305
                        client = oauth2.NewClient(ctx, ts)
×
306
                }
×
307
                githubClient = github.NewClient(client)
×
308
        })
309
        return githubClient
×
310
}
311

312
type samplesRepo struct {
313
        Samples []StarterTemplate `json:"samples"`
314
}
315

316
type StarterTemplate struct {
317
        Name        string `json:"name"`
318
        Description string `json:"description"`
319
        Url         string `json:"url"`
320
        Start       string `json:"start"`
321
}
322

323
func ListSamples(ctx context.Context, client *github.Client) ([]StarterTemplate, error) {
×
324
        owner := "supabase-community"
×
325
        repo := "supabase-samples"
×
326
        path := "samples.json"
×
327
        ref := "main"
×
328
        opts := github.RepositoryContentGetOptions{Ref: ref}
×
329
        file, _, _, err := client.Repositories.GetContents(ctx, owner, repo, path, &opts)
×
330
        if err != nil {
×
331
                return nil, errors.Errorf("failed to list samples: %w", err)
×
332
        }
×
333
        content, err := file.GetContent()
×
334
        if err != nil {
×
335
                return nil, errors.Errorf("failed to decode samples: %w", err)
×
336
        }
×
337
        var data samplesRepo
×
338
        if err := json.Unmarshal([]byte(content), &data); err != nil {
×
339
                return nil, errors.Errorf("failed to unmarshal samples: %w", err)
×
340
        }
×
341
        return data.Samples, nil
×
342
}
343

344
func downloadSample(ctx context.Context, client *github.Client, templateUrl string, fsys afero.Fs) error {
×
345
        fmt.Println("Downloading:", templateUrl)
×
346
        // https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-user-management
×
347
        parsed, err := url.Parse(templateUrl)
×
348
        if err != nil {
×
349
                return errors.Errorf("failed to parse template url: %w", err)
×
350
        }
×
351
        parts := strings.Split(parsed.Path, "/")
×
352
        owner := parts[1]
×
353
        repo := parts[2]
×
354
        ref := parts[4]
×
355
        root := strings.Join(parts[5:], "/")
×
356
        opts := github.RepositoryContentGetOptions{Ref: ref}
×
357
        queue := make([]string, 0)
×
358
        queue = append(queue, root)
×
359
        jq := utils.NewJobQueue(5)
×
360
        for len(queue) > 0 {
×
361
                contentPath := queue[0]
×
362
                queue = queue[1:]
×
363
                _, directory, _, err := client.Repositories.GetContents(ctx, owner, repo, contentPath, &opts)
×
364
                if err != nil {
×
365
                        return errors.Errorf("failed to download template: %w", err)
×
366
                }
×
367
                for _, file := range directory {
×
368
                        switch file.GetType() {
×
369
                        case "file":
×
370
                                path := strings.TrimPrefix(file.GetPath(), root)
×
NEW
371
                                hostPath := filepath.Join(".", filepath.FromSlash(path))
×
372
                                if err := jq.Put(func() error {
×
373
                                        return utils.DownloadFile(ctx, hostPath, file.GetDownloadURL(), fsys)
×
374
                                }); err != nil {
×
375
                                        return err
×
376
                                }
×
377
                        case "dir":
×
378
                                queue = append(queue, file.GetPath())
×
379
                        default:
×
380
                                fmt.Fprintf(os.Stderr, "Ignoring %s: %s\n", file.GetType(), file.GetPath())
×
381
                        }
382
                }
383
        }
384
        return jq.Collect()
×
385
}
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