Skip to content

Commit ba49274

Browse files
committed
Implement custom github URL scheme in Go
This is a reimplementation of the bash code in #3937 (comment) Signed-off-by: Jan Dubois <[email protected]>
1 parent 9d5f469 commit ba49274

File tree

2 files changed

+210
-0
lines changed

2 files changed

+210
-0
lines changed

pkg/limatmpl/locator.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
package limatmpl
55

66
import (
7+
"cmp"
78
"context"
9+
"encoding/json"
810
"errors"
911
"fmt"
1012
"io"
@@ -296,6 +298,109 @@ func InstNameFromYAMLPath(yamlPath string) (string, error) {
296298
return s, nil
297299
}
298300

301+
// transformGitHubURL transforms a github: URL to a raw.githubusercontent.com URL.
302+
// Input format: ORG/REPO[/PATH][@BRANCH]
303+
//
304+
// Examples:
305+
// - github:lima-vm/lima -> https://raw.githubusercontent.com/lima-vm/lima/master/lima.yaml
306+
// - github:lima-vm/lima/examples -> https://raw.githubusercontent.com/lima-vm/lima/master/examples/lima.yaml
307+
// - github:lima-vm/[email protected] -> https://raw.githubusercontent.com/lima-vm/lima/v1.0.0/lima.yaml
308+
// - github:lima-vm/lima/examples/docker.yaml -> https://raw.githubusercontent.com/lima-vm/lima/master/examples/docker.yaml
309+
func transformGitHubURL(ctx context.Context, input string) (string, error) {
310+
// Check for explicit branch specification with @ at the end
311+
var branch string
312+
if idx := strings.LastIndex(input, "@"); idx != -1 {
313+
branch = input[idx+1:]
314+
input = input[:idx]
315+
}
316+
317+
parts := strings.Split(input, "/")
318+
if len(parts) < 2 {
319+
return "", fmt.Errorf("github: URL must be at least ORG/REPO, got %q", input)
320+
}
321+
322+
org := parts[0]
323+
repo := parts[1]
324+
325+
// Extract path (everything after ORG/REPO)
326+
var pathPart string
327+
if len(parts) > 2 {
328+
pathPart = strings.Join(parts[2:], "/")
329+
} else {
330+
pathPart = "lima"
331+
}
332+
333+
// If path ends with /, it's a directory, so append lima
334+
if strings.HasSuffix(pathPart, "/") {
335+
pathPart += "lima"
336+
}
337+
338+
// If the filename (last component) has no extension, add .yaml
339+
filename := path.Base(pathPart)
340+
if !strings.Contains(filename, ".") {
341+
pathPart += ".yaml"
342+
}
343+
344+
// Query default branch if no branch was specified
345+
if branch == "" {
346+
var err error
347+
branch, err = getGitHubDefaultBranch(ctx, org, repo)
348+
if err != nil {
349+
return "", fmt.Errorf("failed to get default branch for %s/%s: %w", org, repo, err)
350+
}
351+
}
352+
353+
return fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s", org, repo, branch, pathPart), nil
354+
}
355+
356+
// getGitHubDefaultBranch queries the GitHub API to get the default branch for a repository.
357+
func getGitHubDefaultBranch(ctx context.Context, org, repo string) (string, error) {
358+
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s", org, repo)
359+
360+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, http.NoBody)
361+
if err != nil {
362+
return "", fmt.Errorf("failed to create request: %w", err)
363+
}
364+
365+
req.Header.Set("User-Agent", "lima")
366+
req.Header.Set("Accept", "application/vnd.github.v3+json")
367+
368+
// Check for GitHub token in environment for authenticated requests (higher rate limit)
369+
token := cmp.Or(os.Getenv("GH_TOKEN"), os.Getenv("GITHUB_TOKEN"))
370+
if token != "" {
371+
req.Header.Set("Authorization", "token "+token)
372+
}
373+
374+
resp, err := http.DefaultClient.Do(req)
375+
if err != nil {
376+
return "", fmt.Errorf("failed to query GitHub API: %w", err)
377+
}
378+
defer resp.Body.Close()
379+
380+
body, err := io.ReadAll(resp.Body)
381+
if err != nil {
382+
return "", fmt.Errorf("failed to read GitHub API response: %w", err)
383+
}
384+
385+
if resp.StatusCode != http.StatusOK {
386+
return "", fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, string(body))
387+
}
388+
389+
var repoData struct {
390+
DefaultBranch string `json:"default_branch"`
391+
}
392+
393+
if err := json.Unmarshal(body, &repoData); err != nil {
394+
return "", fmt.Errorf("failed to parse GitHub API response: %w", err)
395+
}
396+
397+
if repoData.DefaultBranch == "" {
398+
return "", fmt.Errorf("repository %s/%s has no default branch", org, repo)
399+
}
400+
401+
return repoData.DefaultBranch, nil
402+
}
403+
299404
func TransformCustomURL(ctx context.Context, locator string) (string, error) {
300405
u, err := url.Parse(locator)
301406
if err != nil || len(u.Scheme) <= 1 {
@@ -312,6 +417,15 @@ func TransformCustomURL(ctx context.Context, locator string) (string, error) {
312417
return newLocator, nil
313418
}
314419

420+
if u.Scheme == "github" {
421+
newLocator, err := transformGitHubURL(ctx, u.Opaque)
422+
if err != nil {
423+
return "", err
424+
}
425+
logrus.Debugf("GitHub locator %q replaced with %q", locator, newLocator)
426+
return newLocator, nil
427+
}
428+
315429
plugin, err := plugins.Find("url-" + u.Scheme)
316430
if err != nil {
317431
return "", err

pkg/limatmpl/locator_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,99 @@ func TestSeemsFileURL(t *testing.T) {
123123
})
124124
}
125125
}
126+
127+
func TestTransformCustomURL_GitHub(t *testing.T) {
128+
testCases := []struct {
129+
name string
130+
input string
131+
expected string
132+
expectError bool
133+
}{
134+
{
135+
name: "basic org/repo with explicit branch",
136+
input: "github:lima-vm/lima@master",
137+
expected: "https://raw.githubusercontent.com/lima-vm/lima/master/lima.yaml",
138+
},
139+
{
140+
name: "org/repo with path and explicit branch",
141+
input: "github:lima-vm/lima/templates/docker@master",
142+
expected: "https://raw.githubusercontent.com/lima-vm/lima/master/templates/docker.yaml",
143+
},
144+
{
145+
name: "org/repo with path, extension, and explicit branch",
146+
input: "github:lima-vm/lima/templates/docker.yaml@master",
147+
expected: "https://raw.githubusercontent.com/lima-vm/lima/master/templates/docker.yaml",
148+
},
149+
{
150+
name: "org/repo with trailing slash and explicit branch",
151+
input: "github:lima-vm/lima/templates/@main",
152+
expected: "https://raw.githubusercontent.com/lima-vm/lima/main/templates/lima.yaml",
153+
},
154+
{
155+
name: "org/repo with tag version",
156+
input: "github:lima-vm/[email protected]",
157+
expected: "https://raw.githubusercontent.com/lima-vm/lima/v1.0.0/lima.yaml",
158+
},
159+
{
160+
name: "org/repo with path and tag",
161+
input: "github:lima-vm/lima/templates/[email protected]",
162+
expected: "https://raw.githubusercontent.com/lima-vm/lima/v2.0.0/templates/alpine.yaml",
163+
},
164+
{
165+
name: "invalid format - only org",
166+
input: "github:lima-vm",
167+
expectError: true,
168+
},
169+
{
170+
name: "invalid format - empty",
171+
input: "github:",
172+
expectError: true,
173+
},
174+
}
175+
176+
for _, tc := range testCases {
177+
t.Run(tc.name, func(t *testing.T) {
178+
result, err := limatmpl.TransformCustomURL(t.Context(), tc.input)
179+
180+
if tc.expectError {
181+
assert.Assert(t, err != nil, "expected error but got none")
182+
} else {
183+
assert.NilError(t, err)
184+
assert.Equal(t, result, tc.expected)
185+
}
186+
})
187+
}
188+
}
189+
190+
func TestTransformCustomURL_GitHubWithDefaultBranch(t *testing.T) {
191+
// These tests require network access and will query the GitHub API
192+
// Skip if running in an environment without network access
193+
if testing.Short() {
194+
t.Skip("skipping network-dependent test in short mode")
195+
}
196+
197+
testCases := []struct {
198+
name string
199+
input string
200+
expected string
201+
}{
202+
{
203+
name: "basic org/repo queries default branch",
204+
input: "github:lima-vm/lima",
205+
expected: "https://raw.githubusercontent.com/lima-vm/lima/master/lima.yaml",
206+
},
207+
{
208+
name: "org/repo with path queries default branch",
209+
input: "github:lima-vm/lima/templates/docker",
210+
expected: "https://raw.githubusercontent.com/lima-vm/lima/master/templates/docker.yaml",
211+
},
212+
}
213+
214+
for _, tc := range testCases {
215+
t.Run(tc.name, func(t *testing.T) {
216+
result, err := limatmpl.TransformCustomURL(t.Context(), tc.input)
217+
assert.NilError(t, err)
218+
assert.Equal(t, result, tc.expected)
219+
})
220+
}
221+
}

0 commit comments

Comments
 (0)