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

supabase / cli / 13684299921

05 Mar 2025 07:47PM UTC coverage: 57.847% (-0.02%) from 57.869%
13684299921

Pull #3253

github

web-flow
Merge ac7f7cad3 into 468ce0f6e
Pull Request #3253: chore: move function deploy to public package

35 of 49 new or added lines in 3 files covered. (71.43%)

5 existing lines in 1 file now uncovered.

7847 of 13565 relevant lines covered (57.85%)

199.37 hits per line

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

75.65
/pkg/function/deploy.go
1
package function
2

3
import (
4
        "bytes"
5
        "context"
6
        "encoding/json"
7
        "fmt"
8
        "io"
9
        "io/fs"
10
        "mime/multipart"
11
        "os"
12
        "path"
13
        "path/filepath"
14
        "regexp"
15
        "strings"
16

17
        "github.com/go-errors/errors"
18
        "github.com/supabase/cli/pkg/api"
19
        "github.com/supabase/cli/pkg/cast"
20
        "github.com/supabase/cli/pkg/config"
21
        "github.com/supabase/cli/pkg/queue"
22
        "github.com/tidwall/jsonc"
23
)
24

25
var ErrNoDeploy = errors.New("All Functions are up to date.")
26

27
func (s *EdgeRuntimeAPI) Deploy(ctx context.Context, functionConfig config.FunctionConfig, fsys fs.FS) error {
3✔
28
        if s.eszip != nil {
3✔
NEW
29
                return s.UpsertFunctions(ctx, functionConfig)
×
NEW
30
        }
×
31
        var toDeploy []api.FunctionDeployMetadata
3✔
32
        for slug, fc := range functionConfig {
7✔
33
                if !fc.Enabled {
4✔
34
                        fmt.Fprintln(os.Stderr, "Skipped deploying Function:", slug)
×
35
                        continue
×
36
                }
37
                meta := api.FunctionDeployMetadata{
4✔
38
                        Name:           &slug,
4✔
39
                        EntrypointPath: filepath.ToSlash(fc.Entrypoint),
4✔
40
                        ImportMapPath:  cast.Ptr(filepath.ToSlash(fc.ImportMap)),
4✔
41
                        VerifyJwt:      &fc.VerifyJWT,
4✔
42
                }
4✔
43
                files := make([]string, len(fc.StaticFiles))
4✔
44
                for i, sf := range fc.StaticFiles {
4✔
45
                        files[i] = filepath.ToSlash(sf)
×
46
                }
×
47
                meta.StaticPatterns = &files
4✔
48
                toDeploy = append(toDeploy, meta)
4✔
49
        }
50
        if len(toDeploy) == 0 {
3✔
NEW
51
                return errors.New(ErrNoDeploy)
×
52
        } else if len(toDeploy) == 1 {
5✔
53
                param := api.V1DeployAFunctionParams{Slug: toDeploy[0].Name}
2✔
54
                _, err := s.upload(ctx, param, toDeploy[0], fsys)
2✔
55
                return err
2✔
56
        }
2✔
57
        return s.bulkUpload(ctx, toDeploy, fsys)
1✔
58
}
59

60
func (s *EdgeRuntimeAPI) bulkUpload(ctx context.Context, toDeploy []api.FunctionDeployMetadata, fsys fs.FS) error {
1✔
61
        jq := queue.NewJobQueue(s.maxJobs)
1✔
62
        toUpdate := make([]api.BulkUpdateFunctionBody, len(toDeploy))
1✔
63
        for i, meta := range toDeploy {
3✔
64
                fmt.Fprintln(os.Stderr, "Deploying Function:", *meta.Name)
2✔
65
                param := api.V1DeployAFunctionParams{
2✔
66
                        Slug:       meta.Name,
2✔
67
                        BundleOnly: cast.Ptr(true),
2✔
68
                }
2✔
69
                bundle := func() error {
4✔
70
                        resp, err := s.upload(ctx, param, meta, fsys)
2✔
71
                        if err != nil {
2✔
72
                                return err
×
73
                        }
×
74
                        toUpdate[i].Id = resp.Id
2✔
75
                        toUpdate[i].Name = resp.Name
2✔
76
                        toUpdate[i].Slug = resp.Slug
2✔
77
                        toUpdate[i].Version = resp.Version
2✔
78
                        toUpdate[i].EntrypointPath = resp.EntrypointPath
2✔
79
                        toUpdate[i].ImportMap = resp.ImportMap
2✔
80
                        toUpdate[i].ImportMapPath = resp.ImportMapPath
2✔
81
                        toUpdate[i].VerifyJwt = resp.VerifyJwt
2✔
82
                        toUpdate[i].Status = api.BulkUpdateFunctionBodyStatus(resp.Status)
2✔
83
                        toUpdate[i].CreatedAt = resp.CreatedAt
2✔
84
                        return nil
2✔
85
                }
86
                if err := jq.Put(bundle); err != nil {
2✔
87
                        return err
×
88
                }
×
89
        }
90
        if err := jq.Collect(); err != nil {
1✔
91
                return err
×
92
        }
×
93
        if resp, err := s.client.V1BulkUpdateFunctionsWithResponse(ctx, s.project, toUpdate); err != nil {
1✔
94
                return errors.Errorf("failed to bulk update: %w", err)
×
95
        } else if resp.JSON200 == nil {
1✔
96
                return errors.Errorf("unexpected bulk update status %d: %s", resp.StatusCode(), string(resp.Body))
×
97
        }
×
98
        return nil
1✔
99
}
100

101
func (s *EdgeRuntimeAPI) upload(ctx context.Context, param api.V1DeployAFunctionParams, meta api.FunctionDeployMetadata, fsys fs.FS) (*api.DeployFunctionResponse, error) {
4✔
102
        body, w := io.Pipe()
4✔
103
        form := multipart.NewWriter(w)
4✔
104
        ctx, cancel := context.WithCancelCause(ctx)
4✔
105
        go func() {
8✔
106
                defer w.Close()
4✔
107
                defer form.Close()
4✔
108
                if err := writeForm(form, meta, fsys); err != nil {
4✔
109
                        // Since we are streaming files to the POST request body, any errors
×
110
                        // should be propagated to the request context to cancel the upload.
×
111
                        cancel(err)
×
112
                }
×
113
        }()
114
        resp, err := s.client.V1DeployAFunctionWithBodyWithResponse(ctx, s.project, &param, form.FormDataContentType(), body)
4✔
115
        if cause := context.Cause(ctx); cause != ctx.Err() {
4✔
116
                return nil, cause
×
117
        } else if err != nil {
5✔
118
                return nil, errors.Errorf("failed to deploy function: %w", err)
1✔
119
        } else if resp.JSON201 == nil {
4✔
120
                return nil, errors.Errorf("unexpected deploy status %d: %s", resp.StatusCode(), string(resp.Body))
×
121
        }
×
122
        return resp.JSON201, nil
3✔
123
}
124

125
func writeForm(form *multipart.Writer, meta api.FunctionDeployMetadata, fsys fs.FS) error {
7✔
126
        m, err := form.CreateFormField("metadata")
7✔
127
        if err != nil {
7✔
128
                return errors.Errorf("failed to create metadata: %w", err)
×
129
        }
×
130
        enc := json.NewEncoder(m)
3✔
131
        if err := enc.Encode(meta); err != nil {
3✔
132
                return errors.Errorf("failed to encode metadata: %w", err)
×
133
        }
×
134
        addFile := func(srcPath string, w io.Writer) error {
6✔
135
                f, err := fsys.Open(filepath.FromSlash(srcPath))
3✔
136
                if err != nil {
3✔
137
                        return errors.Errorf("failed to read file: %w", err)
×
138
                }
×
139
                defer f.Close()
3✔
140
                if fi, err := f.Stat(); err != nil {
3✔
141
                        return errors.Errorf("failed to stat file: %w", err)
×
142
                } else if fi.IsDir() {
4✔
143
                        return errors.New("file path is a directory: " + srcPath)
1✔
144
                }
1✔
145
                fmt.Fprintf(os.Stderr, "Uploading asset (%s): %s\n", *meta.Name, srcPath)
2✔
146
                r := io.TeeReader(f, w)
2✔
147
                dst, err := form.CreateFormFile("file", srcPath)
2✔
148
                if err != nil {
2✔
149
                        return errors.Errorf("failed to create form: %w", err)
×
150
                }
×
151
                if _, err := io.Copy(dst, r); err != nil {
2✔
152
                        return errors.Errorf("failed to write form: %w", err)
×
153
                }
×
154
                return nil
2✔
155
        }
156
        // Add import map
157
        importMap := ImportMap{}
3✔
158
        if imPath := cast.Val(meta.ImportMapPath, ""); len(imPath) > 0 {
5✔
159
                data, err := fs.ReadFile(fsys, filepath.FromSlash(imPath))
2✔
160
                if err != nil {
3✔
161
                        return errors.Errorf("failed to load import map: %w", err)
1✔
162
                }
1✔
163
                if err := importMap.Parse(data); err != nil {
1✔
164
                        return err
×
165
                }
×
166
                // TODO: replace with addFile once edge runtime supports jsonc
167
                fmt.Fprintf(os.Stderr, "Uploading asset (%s): %s\n", *meta.Name, imPath)
1✔
168
                f, err := form.CreateFormFile("file", imPath)
1✔
169
                if err != nil {
1✔
170
                        return errors.Errorf("failed to create import map: %w", err)
×
171
                }
×
172
                if _, err := f.Write(data); err != nil {
1✔
173
                        return errors.Errorf("failed to write import map: %w", err)
×
174
                }
×
175
        }
176
        // Add static files
177
        patterns := config.Glob(cast.Val(meta.StaticPatterns, []string{}))
2✔
178
        files, err := patterns.Files(fsys)
2✔
179
        if err != nil {
2✔
NEW
180
                fmt.Fprintln(os.Stderr, "WARN:", err)
×
181
        }
×
182
        for _, sfPath := range files {
4✔
183
                if err := addFile(sfPath, io.Discard); err != nil {
3✔
184
                        return err
1✔
185
                }
1✔
186
        }
187
        return walkImportPaths(meta.EntrypointPath, importMap, addFile)
1✔
188
}
189

190
type ImportMap struct {
191
        Imports map[string]string            `json:"imports"`
192
        Scopes  map[string]map[string]string `json:"scopes"`
193
}
194

195
func (m *ImportMap) Parse(data []byte) error {
1✔
196
        data = jsonc.ToJSONInPlace(data)
1✔
197
        decoder := json.NewDecoder(bytes.NewReader(data))
1✔
198
        if err := decoder.Decode(&m); err != nil {
1✔
NEW
199
                return errors.Errorf("failed to parse import map: %w", err)
×
NEW
200
        }
×
201
        return nil
1✔
202
}
203

204
// Ref: https://regex101.com/r/DfBdJA/1
205
var importPathPattern = regexp.MustCompile(`(?i)(?:import|export)\s+(?:{[^{}]+}|.*?)\s*(?:from)?\s*['"](.*?)['"]|import\(\s*['"](.*?)['"]\)`)
206

207
func walkImportPaths(srcPath string, importMap ImportMap, readFile func(curr string, w io.Writer) error) error {
3✔
208
        seen := map[string]struct{}{}
3✔
209
        // DFS because it's more efficient to pop from end of array
3✔
210
        q := make([]string, 1)
3✔
211
        q[0] = srcPath
3✔
212
        for len(q) > 0 {
20✔
213
                curr := q[len(q)-1]
17✔
214
                q = q[:len(q)-1]
17✔
215
                // Assume no file is symlinked
17✔
216
                if _, ok := seen[curr]; ok {
24✔
217
                        continue
7✔
218
                }
219
                seen[curr] = struct{}{}
10✔
220
                // Read into memory for regex match later
10✔
221
                var buf bytes.Buffer
10✔
222
                if err := readFile(curr, &buf); errors.Is(err, os.ErrNotExist) {
13✔
223
                        fmt.Fprintln(os.Stderr, "WARN:", err)
3✔
224
                        continue
3✔
225
                } else if err != nil {
7✔
226
                        return err
×
227
                }
×
228
                // Traverse all modules imported by the current source file
229
                for _, matches := range importPathPattern.FindAllStringSubmatch(buf.String(), -1) {
109✔
230
                        if len(matches) < 3 {
102✔
231
                                continue
×
232
                        }
233
                        // Matches 'from' clause if present, else fallback to 'import'
234
                        mod := matches[1]
102✔
235
                        if len(mod) == 0 {
106✔
236
                                mod = matches[2]
4✔
237
                        }
4✔
238
                        mod = strings.TrimSpace(mod)
102✔
239
                        // Substitute kv from import map
102✔
240
                        for k, v := range importMap.Imports {
154✔
241
                                if strings.HasPrefix(mod, k) {
54✔
242
                                        mod = v + mod[len(k):]
2✔
243
                                }
2✔
244
                        }
245
                        // Deno import path must begin with these prefixes
246
                        if strings.HasPrefix(mod, "./") || strings.HasPrefix(mod, "../") {
122✔
247
                                mod = path.Join(path.Dir(curr), mod)
20✔
248
                        } else if !strings.HasPrefix(mod, "/") {
178✔
249
                                continue
76✔
250
                        }
251
                        if len(path.Ext(mod)) > 0 {
40✔
252
                                // Cleans import path to help detect duplicates
14✔
253
                                q = append(q, path.Clean(mod))
14✔
254
                        }
14✔
255
                }
256
        }
257
        return nil
3✔
258
}
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